aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com>2022-10-27 07:13:47 +0530
committerLibravatar GitHub <noreply@github.com>2022-10-27 01:43:47 +0000
commit81c43ecc3d17e0dbf7ad1d949b6d977f2c65bd48 (patch)
treedfa7c08cb54fb81b7d2e788d350de52c2ebd05d2
parent6.2.1-nightly.30 [skip ci] (diff)
downloadferdium-app-81c43ecc3d17e0dbf7ad1d949b6d977f2c65bd48.tar.gz
ferdium-app-81c43ecc3d17e0dbf7ad1d949b6d977f2c65bd48.tar.zst
ferdium-app-81c43ecc3d17e0dbf7ad1d949b6d977f2c65bd48.zip
fix: 'failed prop' warning in QuickSwitchModal, SettingsNavigation, SettingsWindow and Recipe component tree (#713)
* chore: turn off eslint rule @typescript-eslint/no-useless-constructor to initialize dynamic props & state Co-authored-by: Muhamed <> Co-authored-by: Vijay A <vraravam@users.noreply.github.com>
-rw-r--r--.eslintrc.js1
-rw-r--r--src/components/settings/navigation/SettingsNavigation.tsx (renamed from src/components/settings/navigation/SettingsNavigation.jsx)70
-rw-r--r--src/components/settings/recipes/RecipeItem.tsx (renamed from src/components/settings/recipes/RecipeItem.js)23
-rw-r--r--src/components/settings/recipes/RecipesDashboard.tsx (renamed from src/components/settings/recipes/RecipesDashboard.jsx)65
-rw-r--r--src/components/ui/SearchInput.tsx89
-rw-r--r--src/components/ui/effects/Appear.tsx14
-rw-r--r--src/containers/settings/RecipesScreen.tsx36
-rw-r--r--src/containers/settings/SettingsWindow.tsx25
-rw-r--r--src/features/quickSwitch/Component.tsx (renamed from src/features/quickSwitch/Component.js)102
-rw-r--r--src/models/RecipePreview.ts10
10 files changed, 212 insertions, 223 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index fd4ff7516..585cb3e75 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -73,6 +73,7 @@ module.exports = {
73 // TODO - [TS DEBT] should remove below config once application converted to TS 73 // TODO - [TS DEBT] should remove below config once application converted to TS
74 'react/default-props-match-prop-types': 0, 74 'react/default-props-match-prop-types': 0,
75 'react/require-default-props': 0, 75 'react/require-default-props': 0,
76 '@typescript-eslint/no-useless-constructor': 0,
76 // eslint-plugin-react 77 // eslint-plugin-react
77 'react/destructuring-assignment': 0, 78 'react/destructuring-assignment': 0,
78 'react/button-has-type': 0, 79 'react/button-has-type': 0,
diff --git a/src/components/settings/navigation/SettingsNavigation.jsx b/src/components/settings/navigation/SettingsNavigation.tsx
index e1242a7fe..95c69027c 100644
--- a/src/components/settings/navigation/SettingsNavigation.jsx
+++ b/src/components/settings/navigation/SettingsNavigation.tsx
@@ -1,18 +1,13 @@
1import { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
3import { defineMessages, injectIntl } from 'react-intl';
4import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
5import { RouterStore } from '@superwf/mobx-react-router';
6
7import { NavLink } from 'react-router-dom'; 4import { NavLink } from 'react-router-dom';
5import { StoresProps } from '../../../@types/ferdium-components.types';
8import { 6import {
9 LOCAL_SERVER, 7 LOCAL_SERVER,
10 LIVE_FERDIUM_API, 8 LIVE_FERDIUM_API,
11 LIVE_FRANZ_API, 9 LIVE_FRANZ_API,
12} from '../../../config'; 10} from '../../../config';
13import UIStore from '../../../stores/UIStore';
14import SettingsStore from '../../../stores/SettingsStore';
15import UserStore from '../../../stores/UserStore';
16import globalMessages from '../../../i18n/globalMessages'; 11import globalMessages from '../../../i18n/globalMessages';
17 12
18const messages = defineMessages({ 13const messages = defineMessages({
@@ -50,40 +45,37 @@ const messages = defineMessages({
50 }, 45 },
51}); 46});
52 47
53class SettingsNavigation extends Component { 48interface IProps extends Partial<StoresProps>, WrappedComponentProps {
54 static propTypes = { 49 serviceCount: number;
55 stores: PropTypes.shape({ 50 workspaceCount: number;
56 ui: PropTypes.instanceOf(UIStore).isRequired, 51}
57 user: PropTypes.instanceOf(UserStore).isRequired, 52
58 settings: PropTypes.instanceOf(SettingsStore).isRequired, 53@inject('stores', 'actions')
59 router: PropTypes.instanceOf(RouterStore).isRequired, 54@observer
60 }).isRequired, 55class SettingsNavigation extends Component<IProps> {
61 actions: PropTypes.shape({ 56 constructor(props: IProps) {
62 settings: PropTypes.instanceOf(SettingsStore).isRequired, 57 super(props);
63 }).isRequired, 58 }
64 serviceCount: PropTypes.number.isRequired,
65 workspaceCount: PropTypes.number.isRequired,
66 };
67 59
68 handleLogout() { 60 handleLogout(): void {
69 const isUsingWithoutAccount = 61 const isUsingWithoutAccount =
70 this.props.stores.settings.app.server === LOCAL_SERVER; 62 this.props.stores!.settings.app.server === LOCAL_SERVER;
71 63
72 // Remove current auth token 64 // Remove current auth token
73 localStorage.removeItem('authToken'); 65 localStorage.removeItem('authToken');
74 66
75 if (isUsingWithoutAccount) { 67 if (isUsingWithoutAccount) {
76 // Reset server back to Ferdium API 68 // Reset server back to Ferdium API
77 this.props.actions.settings.update({ 69 this.props.actions!.settings.update({
78 type: 'app', 70 type: 'app',
79 data: { 71 data: {
80 server: LIVE_FERDIUM_API, 72 server: LIVE_FERDIUM_API,
81 }, 73 },
82 }); 74 });
83 } 75 }
84 this.props.stores.user.isLoggingOut = true; 76 this.props.stores!.user.isLoggingOut = true;
85 77
86 this.props.stores.router.push('/auth/welcome'); 78 this.props.stores!.router.push('/auth/welcome');
87 79
88 // Reload Ferdium, otherwise many settings won't sync correctly with the server 80 // Reload Ferdium, otherwise many settings won't sync correctly with the server
89 // after logging into another account 81 // after logging into another account
@@ -91,10 +83,9 @@ class SettingsNavigation extends Component {
91 } 83 }
92 84
93 render() { 85 render() {
94 const { serviceCount, workspaceCount, stores } = this.props; 86 const { serviceCount, workspaceCount, stores, intl } = this.props;
95 const { intl } = this.props; 87 const isUsingWithoutAccount = stores!.settings.app.server === LOCAL_SERVER;
96 const isUsingWithoutAccount = stores.settings.app.server === LOCAL_SERVER; 88 const isUsingFranzServer = stores!.settings.app.server === LIVE_FRANZ_API;
97 const isUsingFranzServer = stores.settings.app.server === LIVE_FRANZ_API;
98 89
99 return ( 90 return (
100 <div className="settings-navigation"> 91 <div className="settings-navigation">
@@ -163,12 +154,12 @@ class SettingsNavigation extends Component {
163 } 154 }
164 > 155 >
165 {intl.formatMessage(globalMessages.settings)} 156 {intl.formatMessage(globalMessages.settings)}
166 {stores.settings.app.automaticUpdates && 157 {stores!.settings.app.automaticUpdates &&
167 (stores.ui.showServicesUpdatedInfoBar || 158 (stores!.ui.showServicesUpdatedInfoBar ||
168 stores.app.updateStatus === 159 stores!.app.updateStatus ===
169 stores.app.updateStatusTypes.AVAILABLE || 160 stores!.app.updateStatusTypes.AVAILABLE ||
170 stores.app.updateStatus === 161 stores!.app.updateStatus ===
171 stores.app.updateStatusTypes.DOWNLOADED) && ( 162 stores!.app.updateStatusTypes.DOWNLOADED) && (
172 <span className="update-available">•</span> 163 <span className="update-available">•</span>
173 )} 164 )}
174 </NavLink> 165 </NavLink>
@@ -195,7 +186,8 @@ class SettingsNavigation extends Component {
195 <span className="settings-navigation__expander" /> 186 <span className="settings-navigation__expander" />
196 <button 187 <button
197 type="button" 188 type="button"
198 to="/auth/logout" 189 // @ts-ignore
190 to="/auth/logout" // TODO - [TS DEBT] Need to check if button take this prop
199 className="settings-navigation__link" 191 className="settings-navigation__link"
200 onClick={this.handleLogout.bind(this)} 192 onClick={this.handleLogout.bind(this)}
201 > 193 >
@@ -208,6 +200,4 @@ class SettingsNavigation extends Component {
208 } 200 }
209} 201}
210 202
211export default injectIntl( 203export default injectIntl(SettingsNavigation);
212 inject('stores', 'actions')(observer(SettingsNavigation)),
213);
diff --git a/src/components/settings/recipes/RecipeItem.js b/src/components/settings/recipes/RecipeItem.tsx
index df5b42222..432e4e6a1 100644
--- a/src/components/settings/recipes/RecipeItem.js
+++ b/src/components/settings/recipes/RecipeItem.tsx
@@ -1,14 +1,17 @@
1import { Component } from 'react'; 1import { Component, MouseEventHandler } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
3import RecipePreview from '../../../models/RecipePreview';
4 4
5import RecipePreviewModel from '../../../models/RecipePreview'; 5interface IProps {
6 recipe: RecipePreview;
7 onClick: MouseEventHandler<HTMLButtonElement>;
8}
6 9
7class RecipeItem extends Component { 10@observer
8 static propTypes = { 11class RecipeItem extends Component<IProps> {
9 recipe: PropTypes.instanceOf(RecipePreviewModel).isRequired, 12 constructor(props: IProps) {
10 onClick: PropTypes.func.isRequired, 13 super(props);
11 }; 14 }
12 15
13 render() { 16 render() {
14 const { recipe, onClick } = this.props; 17 const { recipe, onClick } = this.props;
@@ -18,7 +21,7 @@ class RecipeItem extends Component {
18 {recipe.isDevRecipe && ( 21 {recipe.isDevRecipe && (
19 <span className="recipe-teaser__dev-badge">dev</span> 22 <span className="recipe-teaser__dev-badge">dev</span>
20 )} 23 )}
21 <img src={recipe.icons.svg} className="recipe-teaser__icon" alt="" /> 24 <img src={recipe.icons?.svg} className="recipe-teaser__icon" alt="" />
22 <span className="recipe-teaser__label">{recipe.name}</span> 25 <span className="recipe-teaser__label">{recipe.name}</span>
23 {recipe.aliases && recipe.aliases.length > 0 && ( 26 {recipe.aliases && recipe.aliases.length > 0 && (
24 <span className="recipe-teaser__alias_label"> 27 <span className="recipe-teaser__alias_label">
@@ -30,4 +33,4 @@ class RecipeItem extends Component {
30 } 33 }
31} 34}
32 35
33export default observer(RecipeItem); 36export default RecipeItem;
diff --git a/src/components/settings/recipes/RecipesDashboard.jsx b/src/components/settings/recipes/RecipesDashboard.tsx
index d6150d300..fc687bc79 100644
--- a/src/components/settings/recipes/RecipesDashboard.jsx
+++ b/src/components/settings/recipes/RecipesDashboard.tsx
@@ -1,17 +1,14 @@
1import { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import { observer } from 'mobx-react';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
4import { defineMessages, injectIntl } from 'react-intl';
5import { NavLink } from 'react-router-dom'; 4import { NavLink } from 'react-router-dom';
6 5import withStyles, { WithStylesProps } from 'react-jss';
7import injectSheet from 'react-jss';
8
9import { mdiOpenInNew } from '@mdi/js'; 6import { mdiOpenInNew } from '@mdi/js';
10import Button from '../../ui/button'; 7import Button from '../../ui/button';
11import Input from '../../ui/input/index'; 8import Input from '../../ui/input/index';
12import { H1, H2, H3 } from '../../ui/headline'; 9import { H1, H2, H3 } from '../../ui/headline';
13import SearchInput from '../../ui/SearchInput'; 10import SearchInput from '../../ui/SearchInput';
14import Infobox from '../../ui/Infobox'; 11import Infobox from '../../ui/infobox';
15import RecipeItem from './RecipeItem'; 12import RecipeItem from './RecipeItem';
16import Loader from '../../ui/Loader'; 13import Loader from '../../ui/Loader';
17import Appear from '../../ui/effects/Appear'; 14import Appear from '../../ui/effects/Appear';
@@ -109,28 +106,32 @@ const styles = {
109 }, 106 },
110}; 107};
111 108
112class RecipesDashboard extends Component { 109interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
113 static propTypes = { 110 recipes: RecipePreview[];
114 recipes: MobxPropTypes.arrayOrObservableArray.isRequired, 111 customWebsiteRecipe?: RecipePreview;
115 customWebsiteRecipe: PropTypes.instanceOf(RecipePreview).isRequired, 112 isLoading: boolean;
116 isLoading: PropTypes.bool.isRequired, 113 hasLoadedRecipes: boolean;
117 hasLoadedRecipes: PropTypes.bool.isRequired, 114 showAddServiceInterface: (...args: any[]) => void;
118 showAddServiceInterface: PropTypes.func.isRequired, 115 searchRecipes: (e: string | null) => void;
119 searchRecipes: PropTypes.func.isRequired, 116 resetSearch: () => void;
120 resetSearch: PropTypes.func.isRequired, 117 serviceStatus: string[];
121 serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired, 118 searchNeedle: string | null;
122 searchNeedle: PropTypes.string, 119 recipeFilter?: string;
123 recipeFilter: PropTypes.string, 120 recipeDirectory: string;
124 recipeDirectory: PropTypes.string.isRequired, 121 openRecipeDirectory: () => void;
125 openRecipeDirectory: PropTypes.func.isRequired, 122 openDevDocs: () => void;
126 openDevDocs: PropTypes.func.isRequired, 123}
127 classes: PropTypes.object.isRequired, 124
128 }; 125interface IState {
126 searchNeedle: string | null;
127 recipeFilter: string;
128}
129 129
130 static defaultProps = { 130@observer
131 searchNeedle: '', 131class RecipesDashboard extends Component<IProps, IState> {
132 recipeFilter: 'all', 132 constructor(props: IProps) {
133 }; 133 super(props);
134 }
134 135
135 render() { 136 render() {
136 const { 137 const {
@@ -141,15 +142,15 @@ class RecipesDashboard extends Component {
141 showAddServiceInterface, 142 showAddServiceInterface,
142 searchRecipes, 143 searchRecipes,
143 resetSearch, 144 resetSearch,
144 serviceStatus, 145 serviceStatus = 'all',
145 searchNeedle, 146 searchNeedle = '',
146 recipeFilter, 147 recipeFilter,
147 recipeDirectory, 148 recipeDirectory,
148 openRecipeDirectory, 149 openRecipeDirectory,
149 openDevDocs, 150 openDevDocs,
150 classes, 151 classes,
152 intl,
151 } = this.props; 153 } = this.props;
152 const { intl } = this.props;
153 154
154 const communityRecipes = recipes.filter(r => !r.isDevRecipe); 155 const communityRecipes = recipes.filter(r => !r.isDevRecipe);
155 const devRecipes = recipes.filter(r => r.isDevRecipe); 156 const devRecipes = recipes.filter(r => r.isDevRecipe);
@@ -307,5 +308,5 @@ class RecipesDashboard extends Component {
307} 308}
308 309
309export default injectIntl( 310export default injectIntl(
310 injectSheet(styles, { injectTheme: true })(observer(RecipesDashboard)), 311 withStyles(styles, { injectTheme: true })(RecipesDashboard),
311); 312);
diff --git a/src/components/ui/SearchInput.tsx b/src/components/ui/SearchInput.tsx
index 6a8c4de8f..39b8f95bf 100644
--- a/src/components/ui/SearchInput.tsx
+++ b/src/components/ui/SearchInput.tsx
@@ -1,43 +1,35 @@
1import { ChangeEvent, Component } from 'react'; 1import { ChangeEvent, Component, ReactElement } from 'react';
2import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
3import classnames from 'classnames'; 3import classnames from 'classnames';
4import { debounce } from 'lodash'; 4import { debounce, noop } from 'lodash';
5import { mdiCloseCircleOutline, mdiMagnify } from '@mdi/js'; 5import { mdiCloseCircleOutline, mdiMagnify } from '@mdi/js';
6import Icon from './icon'; 6import Icon from './icon';
7 7
8type Props = { 8interface IProps {
9 value: string; 9 value?: string;
10 placeholder: string; 10 placeholder: string;
11 className: string; 11 className?: string;
12 onChange: (e: ChangeEvent<HTMLInputElement>) => void; 12 onChange?: (e: string) => void;
13 onReset: () => void; 13 onReset: () => void;
14 name: string; 14 name?: string;
15 throttle: boolean; 15 throttle?: boolean;
16 throttleDelay: number; 16 throttleDelay?: number;
17 autoFocus: boolean; 17 autoFocus?: boolean;
18}; 18}
19 19
20// Should this file be converted into the coding style similar to './toggle/index.tsx'? 20interface IState {
21class SearchInput extends Component<Props> { 21 value: string;
22 static defaultProps = { 22}
23 value: '', 23
24 placeholder: '', 24@observer
25 className: '', 25class SearchInput extends Component<IProps, IState> {
26 name: 'searchInput', 26 input: HTMLInputElement | null = null;
27 throttle: false, 27
28 throttleDelay: 250, 28 constructor(props: IProps) {
29 onChange: () => null,
30 onReset: () => null,
31 autoFocus: false,
32 };
33
34 input = null;
35
36 constructor(props: Props) {
37 super(props); 29 super(props);
38 30
39 this.state = { 31 this.state = {
40 value: props.value, 32 value: props.value || '',
41 }; 33 };
42 34
43 this.throttledOnChange = debounce( 35 this.throttledOnChange = debounce(
@@ -46,47 +38,47 @@ class SearchInput extends Component<Props> {
46 ); 38 );
47 } 39 }
48 40
49 componentDidMount() { 41 componentDidMount(): void {
50 const { autoFocus } = this.props; 42 const { autoFocus = false } = this.props;
51 43
52 if (autoFocus) { 44 if (autoFocus && this.input) {
53 // @ts-expect-error Object is possibly 'null'.
54 this.input.focus(); 45 this.input.focus();
55 } 46 }
56 } 47 }
57 48
58 onChange(e: ChangeEvent<HTMLInputElement>) { 49 onChange(e: ChangeEvent<HTMLInputElement>): void {
59 const { throttle, onChange } = this.props; 50 const { throttle = false, onChange = noop } = this.props;
60 const { value } = e.target; 51 const { value } = e.target;
61 this.setState({ value }); 52 this.setState({ value });
62 53
63 if (throttle) { 54 if (throttle) {
64 e.persist(); 55 e.persist();
65 // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'ChangeEvent<HTMLInputElement>'.
66 this.throttledOnChange(value); 56 this.throttledOnChange(value);
67 } else { 57 } else {
68 // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'ChangeEvent<HTMLInputElement>'.
69 onChange(value); 58 onChange(value);
70 } 59 }
71 } 60 }
72 61
73 throttledOnChange(e: ChangeEvent<HTMLInputElement>) { 62 throttledOnChange(e: string): void {
74 const { onChange } = this.props; 63 const { onChange = noop } = this.props;
75 64
76 onChange(e); 65 onChange(e);
77 } 66 }
78 67
79 reset() { 68 reset(): void {
80 const { onReset } = this.props; 69 const { onReset = noop } = this.props;
81 this.setState({ value: '' }); 70 this.setState({ value: '' });
82 71
83 onReset(); 72 onReset();
84 } 73 }
85 74
86 render() { 75 render(): ReactElement {
87 const { className, name, placeholder } = this.props; 76 const {
88 // @ts-expect-error Property 'value' does not exist on type 'Readonly<{}>'. 77 className = '',
89 const { value } = this.state; 78 name = 'searchInput',
79 placeholder = '',
80 } = this.props;
81 const { value = '' } = this.state;
90 82
91 return ( 83 return (
92 <div className={classnames([className, 'search-input'])}> 84 <div className={classnames([className, 'search-input'])}>
@@ -100,13 +92,12 @@ class SearchInput extends Component<Props> {
100 value={value} 92 value={value}
101 onChange={e => this.onChange(e)} 93 onChange={e => this.onChange(e)}
102 ref={ref => { 94 ref={ref => {
103 // @ts-expect-error Type 'HTMLInputElement | null' is not assignable to type 'null'.
104 this.input = ref; 95 this.input = ref;
105 }} 96 }}
106 /> 97 />
107 </label> 98 </label>
108 {value.length > 0 && ( 99 {value.length > 0 && (
109 <span onClick={() => this.reset()}> 100 <span onClick={() => this.reset()} onKeyDown={noop}>
110 <Icon icon={mdiCloseCircleOutline} /> 101 <Icon icon={mdiCloseCircleOutline} />
111 </span> 102 </span>
112 )} 103 )}
@@ -115,4 +106,4 @@ class SearchInput extends Component<Props> {
115 } 106 }
116} 107}
117 108
118export default observer(SearchInput); 109export default SearchInput;
diff --git a/src/components/ui/effects/Appear.tsx b/src/components/ui/effects/Appear.tsx
index 416017c83..bf097b6a6 100644
--- a/src/components/ui/effects/Appear.tsx
+++ b/src/components/ui/effects/Appear.tsx
@@ -1,16 +1,16 @@
1import { ReactNode, useEffect, useState } from 'react'; 1import { ReactElement, ReactNode, useEffect, useState } from 'react';
2import { CSSTransitionGroup } from 'react-transition-group'; 2import { CSSTransitionGroup } from 'react-transition-group';
3 3
4type Props = { 4interface IProps {
5 children: ReactNode; 5 children: ReactNode;
6 transitionName: string; 6 transitionName?: string;
7 className?: string; 7 className?: string;
8}; 8}
9const Appear = ({ 9const Appear = ({
10 children, 10 children,
11 transitionName = 'fadeIn', 11 transitionName = 'fadeIn',
12 className = '', 12 className = '',
13}: Props) => { 13}: IProps): ReactElement | null => {
14 const [mounted, setMounted] = useState(false); 14 const [mounted, setMounted] = useState(false);
15 15
16 useEffect(() => { 16 useEffect(() => {
@@ -36,8 +36,4 @@ const Appear = ({
36 ); 36 );
37}; 37};
38 38
39Appear.defaultProps = {
40 className: '',
41};
42
43export default Appear; 39export default Appear;
diff --git a/src/containers/settings/RecipesScreen.tsx b/src/containers/settings/RecipesScreen.tsx
index fffdd39fa..abbb79b39 100644
--- a/src/containers/settings/RecipesScreen.tsx
+++ b/src/containers/settings/RecipesScreen.tsx
@@ -16,27 +16,30 @@ import RecipePreview from '../../models/RecipePreview';
16import { openPath } from '../../helpers/url-helpers'; 16import { openPath } from '../../helpers/url-helpers';
17import withParams from '../../components/util/WithParams'; 17import withParams from '../../components/util/WithParams';
18 18
19interface RecipesScreenProps extends StoresProps { 19interface IProps extends Partial<StoresProps> {
20 params: Params; 20 params: Params;
21} 21}
22 22
23class RecipesScreen extends Component<RecipesScreenProps> { 23interface IState {
24 state: { 24 needle: string | null;
25 needle: string | null; 25 currentFilter: string;
26 currentFilter: string; 26}
27 } = {
28 needle: null,
29 currentFilter: 'featured',
30 };
31 27
28@inject('stores', 'actions')
29@observer
30class RecipesScreen extends Component<IProps, IState> {
32 autorunDisposer: IReactionDisposer | null = null; 31 autorunDisposer: IReactionDisposer | null = null;
33 32
34 customRecipes: Recipe[] = []; 33 customRecipes: Recipe[] = [];
35 34
36 constructor(props: RecipesScreenProps) { 35 constructor(props: IProps) {
37 super(props); 36 super(props);
38 37
39 this.customRecipes = readJsonSync(asarRecipesPath('all.json')); 38 this.customRecipes = readJsonSync(asarRecipesPath('all.json'));
39 this.state = {
40 needle: null,
41 currentFilter: 'featured',
42 };
40 } 43 }
41 44
42 componentDidMount(): void { 45 componentDidMount(): void {
@@ -55,7 +58,7 @@ class RecipesScreen extends Component<RecipesScreenProps> {
55 } 58 }
56 59
57 componentWillUnmount(): void { 60 componentWillUnmount(): void {
58 this.props.stores.services.resetStatus(); 61 this.props.stores!.services.resetStatus();
59 62
60 if (typeof this.autorunDisposer === 'function') { 63 if (typeof this.autorunDisposer === 'function') {
61 this.autorunDisposer(); 64 this.autorunDisposer();
@@ -66,7 +69,7 @@ class RecipesScreen extends Component<RecipesScreenProps> {
66 if (needle === '') { 69 if (needle === '') {
67 this.resetSearch(); 70 this.resetSearch();
68 } else { 71 } else {
69 const { search } = this.props.actions.recipePreview; 72 const { search } = this.props.actions!.recipePreview;
70 this.setState({ needle }); 73 this.setState({ needle });
71 search({ needle }); 74 search({ needle });
72 } 75 }
@@ -106,10 +109,8 @@ class RecipesScreen extends Component<RecipesScreenProps> {
106 } 109 }
107 110
108 render(): ReactElement { 111 render(): ReactElement {
109 const { recipePreviews, recipes, services } = this.props.stores; 112 const { recipePreviews, recipes, services } = this.props.stores!;
110 113 const { app: appActions, service: serviceActions } = this.props.actions!;
111 const { app: appActions, service: serviceActions } = this.props.actions;
112
113 const filter = this.state.currentFilter; 114 const filter = this.state.currentFilter;
114 115
115 let recipeFilter; 116 let recipeFilter;
@@ -163,7 +164,6 @@ class RecipesScreen extends Component<RecipesScreenProps> {
163 recipes={allRecipes} 164 recipes={allRecipes}
164 customWebsiteRecipe={customWebsiteRecipe} 165 customWebsiteRecipe={customWebsiteRecipe}
165 isLoading={isLoading} 166 isLoading={isLoading}
166 addedServiceCount={services.all.length}
167 hasLoadedRecipes={ 167 hasLoadedRecipes={
168 recipePreviews.featuredRecipePreviewsRequest.wasExecuted 168 recipePreviews.featuredRecipePreviewsRequest.wasExecuted
169 } 169 }
@@ -184,4 +184,4 @@ class RecipesScreen extends Component<RecipesScreenProps> {
184 } 184 }
185} 185}
186 186
187export default withParams(inject('stores', 'actions')(observer(RecipesScreen))); 187export default withParams(RecipesScreen);
diff --git a/src/containers/settings/SettingsWindow.tsx b/src/containers/settings/SettingsWindow.tsx
index 93bb08c7c..d2cdf3eb3 100644
--- a/src/containers/settings/SettingsWindow.tsx
+++ b/src/containers/settings/SettingsWindow.tsx
@@ -1,20 +1,23 @@
1import { inject, observer } from 'mobx-react'; 1import { inject, observer } from 'mobx-react';
2import { Component, ReactPortal } from 'react'; 2import { Component, ReactElement, ReactPortal } from 'react';
3import ReactDOM from 'react-dom'; 3import ReactDOM from 'react-dom';
4import { Outlet } from 'react-router-dom'; 4import { Outlet } from 'react-router-dom';
5
6import { StoresProps } from '../../@types/ferdium-components.types'; 5import { StoresProps } from '../../@types/ferdium-components.types';
7import Navigation from '../../components/settings/navigation/SettingsNavigation'; 6import Navigation from '../../components/settings/navigation/SettingsNavigation';
8import Layout from '../../components/settings/SettingsLayout'; 7import Layout from '../../components/settings/SettingsLayout';
9import ErrorBoundary from '../../components/util/ErrorBoundary'; 8import ErrorBoundary from '../../components/util/ErrorBoundary';
10import { workspaceStore } from '../../features/workspaces'; 9import { workspaceStore } from '../../features/workspaces';
11 10
12class SettingsContainer extends Component<StoresProps> { 11interface IProps extends Partial<StoresProps> {}
13 portalRoot: any; 12
13@inject('stores', 'actions')
14@observer
15class SettingsContainer extends Component<IProps> {
16 portalRoot: HTMLElement | null;
14 17
15 el: HTMLDivElement; 18 el: HTMLDivElement;
16 19
17 constructor(props: StoresProps) { 20 constructor(props: IProps) {
18 super(props); 21 super(props);
19 22
20 this.portalRoot = document.querySelector('#portalContainer'); 23 this.portalRoot = document.querySelector('#portalContainer');
@@ -22,7 +25,9 @@ class SettingsContainer extends Component<StoresProps> {
22 } 25 }
23 26
24 componentDidMount(): void { 27 componentDidMount(): void {
25 this.portalRoot.append(this.el); 28 if (this.portalRoot) {
29 this.portalRoot.append(this.el);
30 }
26 } 31 }
27 32
28 componentWillUnmount(): void { 33 componentWillUnmount(): void {
@@ -31,11 +36,11 @@ class SettingsContainer extends Component<StoresProps> {
31 36
32 render(): ReactPortal { 37 render(): ReactPortal {
33 const { stores } = this.props; 38 const { stores } = this.props;
34 const { closeSettings } = this.props.actions.ui; 39 const { closeSettings } = this.props.actions!.ui;
35 40
36 const navigation = ( 41 const navigation: ReactElement = (
37 <Navigation 42 <Navigation
38 serviceCount={stores.services.all.length} 43 serviceCount={stores!.services.all.length}
39 workspaceCount={workspaceStore.workspaces.length} 44 workspaceCount={workspaceStore.workspaces.length}
40 /> 45 />
41 ); 46 );
@@ -51,4 +56,4 @@ class SettingsContainer extends Component<StoresProps> {
51 } 56 }
52} 57}
53 58
54export default inject('stores', 'actions')(observer(SettingsContainer)); 59export default SettingsContainer;
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.tsx
index 16da22dce..fb85d61e1 100644
--- a/src/features/quickSwitch/Component.js
+++ b/src/features/quickSwitch/Component.tsx
@@ -1,17 +1,16 @@
1import { Component, createRef } from 'react'; 1import { ChangeEvent, Component, createRef, ReactElement } from 'react';
2import { getCurrentWindow } from '@electron/remote'; 2import { getCurrentWindow } from '@electron/remote';
3import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react'; 3import { observer, inject } from 'mobx-react';
5import { reaction } from 'mobx'; 4import { reaction } from 'mobx';
6import injectSheet from 'react-jss'; 5import withStyles, { WithStylesProps } from 'react-jss';
7import { defineMessages, injectIntl } from 'react-intl'; 6import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
8import { compact, invoke } from 'lodash'; 7import { compact, invoke, noop } from 'lodash';
9 8import { StoresProps } from '../../@types/ferdium-components.types';
9import Service from '../../models/Service';
10import Input from '../../components/ui/input/index'; 10import Input from '../../components/ui/input/index';
11import { H1 } from '../../components/ui/headline'; 11import { H1 } from '../../components/ui/headline';
12import Modal from '../../components/ui/Modal'; 12import Modal from '../../components/ui/Modal';
13import { state as ModalState } from './store'; 13import { state as ModalState } from './store';
14import ServicesStore from '../../stores/ServicesStore';
15 14
16const messages = defineMessages({ 15const messages = defineMessages({
17 title: { 16 title: {
@@ -75,17 +74,20 @@ const styles = theme => ({
75 }, 74 },
76}); 75});
77 76
78class QuickSwitchModal extends Component { 77interface IProps
79 static propTypes = { 78 extends WithStylesProps<typeof styles>,
80 classes: PropTypes.object.isRequired, 79 Partial<StoresProps>,
81 }; 80 WrappedComponentProps {}
82 81
83 state = { 82interface IState {
84 selected: 0, 83 selected: number;
85 search: '', 84 search: string;
86 wasPrevVisible: false, 85 wasPrevVisible: boolean;
87 }; 86}
88 87
88@inject('stores', 'actions')
89@observer
90class QuickSwitchModal extends Component<IProps, IState> {
89 ARROW_DOWN = 40; 91 ARROW_DOWN = 40;
90 92
91 ARROW_UP = 38; 93 ARROW_UP = 38;
@@ -94,13 +96,19 @@ class QuickSwitchModal extends Component {
94 96
95 TAB = 9; 97 TAB = 9;
96 98
97 inputRef = createRef(); 99 inputRef = createRef<HTMLDivElement>();
98 100
99 serviceElements = {}; 101 serviceElements = {};
100 102
101 constructor(props) { 103 constructor(props) {
102 super(props); 104 super(props);
103 105
106 this.state = {
107 selected: 0,
108 search: '',
109 wasPrevVisible: false,
110 };
111
104 this._handleKeyDown = this._handleKeyDown.bind(this); 112 this._handleKeyDown = this._handleKeyDown.bind(this);
105 this._handleSearchUpdate = this._handleSearchUpdate.bind(this); 113 this._handleSearchUpdate = this._handleSearchUpdate.bind(this);
106 this._handleVisibilityChange = this._handleVisibilityChange.bind(this); 114 this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
@@ -115,46 +123,46 @@ class QuickSwitchModal extends Component {
115 } 123 }
116 124
117 // Add global keydown listener when component mounts 125 // Add global keydown listener when component mounts
118 componentDidMount() { 126 componentDidMount(): void {
119 document.addEventListener('keydown', this._handleKeyDown); 127 document.addEventListener('keydown', this._handleKeyDown);
120 } 128 }
121 129
122 // Remove global keydown listener when component unmounts 130 // Remove global keydown listener when component unmounts
123 componentWillUnmount() { 131 componentWillUnmount(): void {
124 document.removeEventListener('keydown', this._handleKeyDown); 132 document.removeEventListener('keydown', this._handleKeyDown);
125 } 133 }
126 134
127 // Get currently shown services 135 // Get currently shown services
128 services() { 136 services(): Service[] {
129 let services = []; 137 let services: Service[] = [];
130 if ( 138 if (
131 this.state.search && 139 this.state.search &&
132 compact(invoke(this.state.search, 'match', /^[\da-z]/i)).length > 0 140 compact(invoke(this.state.search, 'match', /^[\da-z]/i)).length > 0
133 ) { 141 ) {
134 // Apply simple search algorythm to list of all services 142 // Apply simple search algorythm to list of all services
135 services = this.props.stores.services.allDisplayed; 143 services = this.props.stores!.services.allDisplayed;
136 services = services.filter( 144 services = services.filter(
137 service => 145 service =>
138 service.name.toLowerCase().search(this.state.search.toLowerCase()) !== 146 service.name.toLowerCase().search(this.state.search.toLowerCase()) !==
139 -1, 147 -1,
140 ); 148 );
141 } else if (this.props.stores.services.allDisplayed.length > 0) { 149 } else if (this.props.stores!.services.allDisplayed.length > 0) {
142 // Add the currently active service first 150 // Add the currently active service first
143 const currentService = this.props.stores.services.active; 151 const currentService = this.props.stores!.services.active;
144 if (currentService) { 152 if (currentService) {
145 services.push(currentService); 153 services.push(currentService);
146 } 154 }
147 155
148 // Add last used services to services array 156 // Add last used services to services array
149 for (const service of this.props.stores.services.lastUsedServices) { 157 for (const service of this.props.stores!.services.lastUsedServices) {
150 const tempService = this.props.stores.services.one(service); 158 const tempService = this.props.stores!.services.one(service);
151 if (tempService && !services.includes(tempService)) { 159 if (tempService && !services.includes(tempService)) {
152 services.push(tempService); 160 services.push(tempService);
153 } 161 }
154 } 162 }
155 163
156 // Add all other services in the default order 164 // Add all other services in the default order
157 for (const service of this.props.stores.services.allDisplayed) { 165 for (const service of this.props.stores!.services.allDisplayed) {
158 if (!services.includes(service)) { 166 if (!services.includes(service)) {
159 services.push(service); 167 services.push(service);
160 } 168 }
@@ -164,10 +172,10 @@ class QuickSwitchModal extends Component {
164 return services; 172 return services;
165 } 173 }
166 174
167 openService(index) { 175 openService(index): void {
168 // Open service 176 // Open service
169 const service = this.services()[index]; 177 const service = this.services()[index];
170 this.props.actions.service.setActive({ serviceId: service.id }); 178 this.props.actions!.service.setActive({ serviceId: service.id });
171 179
172 // Reset and close modal 180 // Reset and close modal
173 this.setState({ 181 this.setState({
@@ -179,7 +187,7 @@ class QuickSwitchModal extends Component {
179 187
180 // Change the selected service 188 // Change the selected service
181 // factor should be -1 or 1 189 // factor should be -1 or 1
182 changeSelected(factor) { 190 changeSelected(factor: number): any {
183 this.setState(state => { 191 this.setState(state => {
184 let newSelected = state.selected + factor; 192 let newSelected = state.selected + factor;
185 const services = this.services().length; 193 const services = this.services().length;
@@ -204,7 +212,7 @@ class QuickSwitchModal extends Component {
204 } 212 }
205 213
206 // Handle global key presses to change the selection 214 // Handle global key presses to change the selection
207 _handleKeyDown(event) { 215 _handleKeyDown(event: KeyboardEvent): void {
208 if (ModalState.isModalVisible) { 216 if (ModalState.isModalVisible) {
209 switch (event.keyCode) { 217 switch (event.keyCode) {
210 case this.ARROW_DOWN: 218 case this.ARROW_DOWN:
@@ -230,13 +238,13 @@ class QuickSwitchModal extends Component {
230 } 238 }
231 239
232 // Handle update of the search query 240 // Handle update of the search query
233 _handleSearchUpdate(evt) { 241 _handleSearchUpdate(event: ChangeEvent<HTMLInputElement>): void {
234 this.setState({ 242 this.setState({
235 search: evt.target.value, 243 search: event.target.value,
236 }); 244 });
237 } 245 }
238 246
239 _handleVisibilityChange() { 247 _handleVisibilityChange(): void {
240 const { isModalVisible } = ModalState; 248 const { isModalVisible } = ModalState;
241 249
242 if (isModalVisible && !this.state.wasPrevVisible) { 250 if (isModalVisible && !this.state.wasPrevVisible) {
@@ -273,21 +281,16 @@ class QuickSwitchModal extends Component {
273 } 281 }
274 282
275 // Close this modal 283 // Close this modal
276 close() { 284 close(): void {
277 ModalState.isModalVisible = false; 285 ModalState.isModalVisible = false;
278 } 286 }
279 287
280 render() { 288 render(): ReactElement {
281 const { isModalVisible } = ModalState; 289 const { isModalVisible } = ModalState;
282
283 const { openService } = this; 290 const { openService } = this;
284 291 const { classes, intl } = this.props;
285 const { classes } = this.props;
286
287 const services = this.services(); 292 const services = this.services();
288 293
289 const { intl } = this.props;
290
291 return ( 294 return (
292 <Modal 295 <Modal
293 isOpen={isModalVisible} 296 isOpen={isModalVisible}
@@ -316,6 +319,7 @@ class QuickSwitchModal extends Component {
316 : '' 319 : ''
317 } service`} 320 } service`}
318 onClick={() => openService(index)} 321 onClick={() => openService(index)}
322 onKeyDown={noop}
319 key={service.id} 323 key={service.id}
320 ref={el => { 324 ref={el => {
321 this.serviceElements[index] = el; 325 this.serviceElements[index] = el;
@@ -340,18 +344,6 @@ class QuickSwitchModal extends Component {
340 } 344 }
341} 345}
342 346
343QuickSwitchModal.propTypes = {
344 stores: PropTypes.shape({
345 services: PropTypes.instanceOf(ServicesStore).isRequired,
346 }).isRequired,
347 actions: PropTypes.shape({
348 service: PropTypes.instanceOf(ServicesStore).isRequired,
349 }).isRequired,
350 classes: PropTypes.object.isRequired,
351};
352
353export default injectIntl( 347export default injectIntl(
354 injectSheet(styles, { injectTheme: true })( 348 withStyles(styles, { injectTheme: true })(QuickSwitchModal),
355 inject('stores', 'actions')(observer(QuickSwitchModal)),
356 ),
357); 349);
diff --git a/src/models/RecipePreview.ts b/src/models/RecipePreview.ts
index fb8cb3e3e..33b5e1432 100644
--- a/src/models/RecipePreview.ts
+++ b/src/models/RecipePreview.ts
@@ -4,6 +4,10 @@ interface IRecipePreview {
4 icon: string; 4 icon: string;
5 featured: boolean; 5 featured: boolean;
6 aliases: string[]; 6 aliases: string[];
7 isDevRecipe?: boolean;
8 icons?: {
9 svg: string;
10 };
7} 11}
8 12
9export default class RecipePreview { 13export default class RecipePreview {
@@ -17,6 +21,12 @@ export default class RecipePreview {
17 21
18 aliases: string[] = []; 22 aliases: string[] = [];
19 23
24 isDevRecipe?: boolean;
25
26 icons?: {
27 svg: string;
28 };
29
20 constructor(data: IRecipePreview) { 30 constructor(data: IRecipePreview) {
21 if (!data) { 31 if (!data) {
22 throw new Error('RecipePreview config not valid'); 32 throw new Error('RecipePreview config not valid');