aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com>2022-11-20 17:35:21 +0530
committerLibravatar GitHub <noreply@github.com>2022-11-20 17:35:21 +0530
commit86f9ab693dcad951271f727046214b03d91ebd69 (patch)
tree3d32ff4814b5484495b811c5fe0ebea4805f4e55
parent6.2.1-nightly.47 [skip ci] (diff)
downloadferdium-app-86f9ab693dcad951271f727046214b03d91ebd69.tar.gz
ferdium-app-86f9ab693dcad951271f727046214b03d91ebd69.tar.zst
ferdium-app-86f9ab693dcad951271f727046214b03d91ebd69.zip
Transform Todo feature, ServiceBarTargetUrl, ServiceIcon, TeamDashboard, Slider, Loader & WorkspaceSwitchningIndicator into ts (#782)
-rw-r--r--src/components/settings/settings/EditSettingsForm.tsx2
-rw-r--r--src/components/settings/team/TeamDashboard.tsx (renamed from src/components/settings/team/TeamDashboard.js)139
-rw-r--r--src/components/ui/FullscreenLoader/index.js52
-rw-r--r--src/components/ui/FullscreenLoader/index.tsx54
-rw-r--r--src/components/ui/Loader.tsx27
-rw-r--r--src/components/ui/ServiceIcon.tsx (renamed from src/components/ui/ServiceIcon.js)32
-rw-r--r--src/components/ui/Slider.tsx (renamed from src/components/ui/Slider.js)50
-rw-r--r--src/components/ui/StatusBarTargetUrl.tsx (renamed from src/components/ui/StatusBarTargetUrl.js)24
-rw-r--r--src/components/ui/WebviewLoader/index.js37
-rw-r--r--src/components/ui/WebviewLoader/index.tsx44
-rw-r--r--src/components/ui/WebviewLoader/styles.ts9
-rw-r--r--src/features/todos/actions.ts14
-rw-r--r--src/features/todos/components/TodosWebview.tsx (renamed from src/features/todos/components/TodosWebview.js)112
-rw-r--r--src/features/todos/containers/TodosScreen.tsx (renamed from src/features/todos/containers/TodosScreen.js)41
-rw-r--r--src/features/utils/FeatureStore.ts (renamed from src/features/utils/FeatureStore.js)24
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.tsx (renamed from src/features/workspaces/components/WorkspaceSwitchingIndicator.js)46
-rw-r--r--src/helpers/url-helpers.ts13
-rw-r--r--src/stores/FeaturesStore.ts17
18 files changed, 369 insertions, 368 deletions
diff --git a/src/components/settings/settings/EditSettingsForm.tsx b/src/components/settings/settings/EditSettingsForm.tsx
index 8ccad9e49..0a05cb0e1 100644
--- a/src/components/settings/settings/EditSettingsForm.tsx
+++ b/src/components/settings/settings/EditSettingsForm.tsx
@@ -650,7 +650,7 @@ class EditSettingsForm extends Component<IProps, IState> {
650 <> 650 <>
651 <Slider 651 <Slider
652 type="number" 652 type="number"
653 onChange={e => this.submit(e)} 653 onSliderChange={e => this.submit(e)}
654 field={form.$('grayscaleServicesDim')} 654 field={form.$('grayscaleServicesDim')}
655 /> 655 />
656 <Hr /> 656 <Hr />
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.tsx
index 538a9a10c..3ef55fac6 100644
--- a/src/components/settings/team/TeamDashboard.js
+++ b/src/components/settings/team/TeamDashboard.tsx
@@ -1,11 +1,9 @@
1import { Component } from 'react'; 1import { Component, ReactElement } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 3import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
5import ReactTooltip from 'react-tooltip'; 4import ReactTooltip from 'react-tooltip';
6import injectSheet from 'react-jss'; 5import withStyles, { WithStylesProps } from 'react-jss';
7import classnames from 'classnames'; 6import classnames from 'classnames';
8
9import Loader from '../../ui/Loader'; 7import Loader from '../../ui/Loader';
10import Button from '../../ui/button'; 8import Button from '../../ui/button';
11import Infobox from '../../ui/Infobox'; 9import Infobox from '../../ui/Infobox';
@@ -99,17 +97,17 @@ const styles = {
99 }, 97 },
100}; 98};
101 99
102class TeamDashboard extends Component { 100interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
103 static propTypes = { 101 isLoading: boolean;
104 isLoading: PropTypes.bool.isRequired, 102 userInfoRequestFailed: boolean;
105 userInfoRequestFailed: PropTypes.bool.isRequired, 103 retryUserInfoRequest: () => void;
106 retryUserInfoRequest: PropTypes.func.isRequired, 104 openTeamManagement: () => void;
107 openTeamManagement: PropTypes.func.isRequired, 105 server: string;
108 classes: PropTypes.object.isRequired, 106}
109 server: PropTypes.string.isRequired,
110 };
111 107
112 render() { 108@observer
109class TeamDashboard extends Component<IProps> {
110 render(): ReactElement {
113 const { 111 const {
114 isLoading, 112 isLoading,
115 userInfoRequestFailed, 113 userInfoRequestFailed,
@@ -117,68 +115,65 @@ class TeamDashboard extends Component {
117 openTeamManagement, 115 openTeamManagement,
118 classes, 116 classes,
119 server, 117 server,
118 intl,
120 } = this.props; 119 } = this.props;
121 const { intl } = this.props;
122 120
123 if (server === LIVE_FRANZ_API) { 121 return server === LIVE_FRANZ_API ? (
124 return ( 122 <div className="settings__main">
125 <div className="settings__main"> 123 <div className="settings__header">
126 <div className="settings__header"> 124 <span className="settings__header-item">
127 <span className="settings__header-item"> 125 {intl.formatMessage(messages.headline)}
128 {intl.formatMessage(messages.headline)} 126 </span>
129 </span> 127 </div>
130 </div> 128 <div className="settings__body">
131 <div className="settings__body"> 129 {isLoading && <Loader />}
132 {isLoading && <Loader />}
133 130
134 {!isLoading && userInfoRequestFailed && ( 131 {!isLoading && userInfoRequestFailed && (
135 <Infobox 132 <Infobox
136 icon="alert" 133 icon="alert"
137 type="danger" 134 type="danger"
138 ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)} 135 ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)}
139 ctaLoading={isLoading} 136 ctaLoading={isLoading}
140 ctaOnClick={retryUserInfoRequest} 137 ctaOnClick={retryUserInfoRequest}
141 > 138 >
142 {intl.formatMessage(messages.userInfoRequestFailed)} 139 {intl.formatMessage(messages.userInfoRequestFailed)}
143 </Infobox> 140 </Infobox>
144 )} 141 )}
145 142
146 {!userInfoRequestFailed && !isLoading && ( 143 {!userInfoRequestFailed && !isLoading && (
147 <> 144 <>
148 <H1 145 <H1
149 className={classnames({ 146 className={classnames({
150 [classes.headline]: true, 147 [classes.headline]: true,
151 [classes.headlineWithSpacing]: true, 148 [classes.headlineWithSpacing]: true,
152 })} 149 })}
153 > 150 >
154 {intl.formatMessage(messages.contentHeadline)} 151 {intl.formatMessage(messages.contentHeadline)}
155 </H1> 152 </H1>
156 <div className={classes.container}> 153 <div className={classes.container}>
157 <div className={classes.content}> 154 <div className={classes.content}>
158 <p>{intl.formatMessage(messages.intro)}</p> 155 <p>{intl.formatMessage(messages.intro)}</p>
159 <p>{intl.formatMessage(messages.copy)}</p> 156 <p>{intl.formatMessage(messages.copy)}</p>
160 </div>
161 <img
162 className={classes.image}
163 src="https://cdn.franzinfra.com/announcements/assets/teams.png"
164 alt="Ferdium for Teams"
165 />
166 </div>
167 <div className={classes.buttonContainer}>
168 <Button
169 label={intl.formatMessage(messages.manageButton)}
170 onClick={openTeamManagement}
171 className={classes.cta}
172 />
173 </div> 157 </div>
174 </> 158 <img
175 )} 159 className={classes.image}
176 </div> 160 src="https://cdn.franzinfra.com/announcements/assets/teams.png"
177 <ReactTooltip place="right" type="dark" effect="solid" /> 161 alt="Ferdium for Teams"
162 />
163 </div>
164 <div className={classes.buttonContainer}>
165 <Button
166 label={intl.formatMessage(messages.manageButton)}
167 onClick={openTeamManagement}
168 className={classes.cta}
169 />
170 </div>
171 </>
172 )}
178 </div> 173 </div>
179 ); 174 <ReactTooltip place="right" type="dark" effect="solid" />
180 } 175 </div>
181 return ( 176 ) : (
182 <div className="settings__main"> 177 <div className="settings__main">
183 <div className="settings__header"> 178 <div className="settings__header">
184 <span className="settings__header-item"> 179 <span className="settings__header-item">
@@ -205,5 +200,5 @@ class TeamDashboard extends Component {
205} 200}
206 201
207export default injectIntl( 202export default injectIntl(
208 injectSheet(styles, { injectTheme: true })(observer(TeamDashboard)), 203 withStyles(styles, { injectTheme: true })(TeamDashboard),
209); 204);
diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js
deleted file mode 100644
index f8c6b92ee..000000000
--- a/src/components/ui/FullscreenLoader/index.js
+++ /dev/null
@@ -1,52 +0,0 @@
1import { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectStyle from 'react-jss';
5import classnames from 'classnames';
6
7import Loader from '../Loader';
8
9import styles from './styles';
10import { H1 } from '../headline';
11
12class FullscreenLoader extends Component {
13 static propTypes = {
14 className: PropTypes.string,
15 title: PropTypes.string,
16 classes: PropTypes.object.isRequired,
17 theme: PropTypes.object.isRequired,
18 spinnerColor: PropTypes.string,
19 children: PropTypes.node,
20 };
21
22 static defaultProps = {
23 className: null,
24 spinnerColor: null,
25 children: null,
26 title: null,
27 };
28
29 render() {
30 const { classes, title, children, spinnerColor, className, theme } =
31 this.props;
32
33 return (
34 <div className={classes.wrapper}>
35 <div
36 className={classnames({
37 [`${classes.component}`]: true,
38 [`${className}`]: className,
39 })}
40 >
41 <H1 className={classes.title}>{title}</H1>
42 <Loader color={spinnerColor || theme.colorFullscreenLoaderSpinner} />
43 {children && <div className={classes.content}>{children}</div>}
44 </div>
45 </div>
46 );
47 }
48}
49
50export default injectStyle(styles, { injectTheme: true })(
51 observer(FullscreenLoader),
52);
diff --git a/src/components/ui/FullscreenLoader/index.tsx b/src/components/ui/FullscreenLoader/index.tsx
new file mode 100644
index 000000000..002ee7932
--- /dev/null
+++ b/src/components/ui/FullscreenLoader/index.tsx
@@ -0,0 +1,54 @@
1import { Component, ReactElement, ReactNode } from 'react';
2import { observer } from 'mobx-react';
3import withStyles, { WithStylesProps } from 'react-jss';
4import classnames from 'classnames';
5import Loader from '../Loader';
6import styles from './styles';
7import { H1 } from '../headline';
8import { Theme } from '../../../themes';
9
10interface IProps extends WithStylesProps<typeof styles> {
11 className?: string;
12 title?: string;
13 theme?: Theme;
14 spinnerColor?: string;
15 loaded?: boolean;
16 children?: ReactNode;
17}
18
19@observer
20class FullscreenLoader extends Component<IProps> {
21 render(): ReactElement {
22 const {
23 classes,
24 theme = '',
25 className = '',
26 spinnerColor = '',
27 children = null,
28 title = '',
29 loaded = false,
30 } = this.props;
31
32 return (
33 <div className={classes.wrapper}>
34 <div
35 className={classnames({
36 [`${classes.component}`]: true,
37 [`${className}`]: className,
38 })}
39 >
40 <H1 className={classes.title}>{title}</H1>
41 <Loader
42 color={
43 spinnerColor || (theme && theme.colorFullscreenLoaderSpinner)
44 }
45 loaded={loaded}
46 />
47 {children && <div className={classes.content}>{children}</div>}
48 </div>
49 </div>
50 );
51 }
52}
53
54export default withStyles(styles, { injectTheme: true })(FullscreenLoader);
diff --git a/src/components/ui/Loader.tsx b/src/components/ui/Loader.tsx
index 5e78ed47a..ebb437d9d 100644
--- a/src/components/ui/Loader.tsx
+++ b/src/components/ui/Loader.tsx
@@ -1,4 +1,4 @@
1import { Component, PropsWithChildren } from 'react'; 1import { Component, ReactElement, ReactNode } from 'react';
2import { observer, inject } from 'mobx-react'; 2import { observer, inject } from 'mobx-react';
3import Loader from 'react-loader'; 3import Loader from 'react-loader';
4 4
@@ -9,31 +9,30 @@ interface IProps {
9 color?: string; 9 color?: string;
10 loaded?: boolean; 10 loaded?: boolean;
11 stores?: FerdiumStores; 11 stores?: FerdiumStores;
12 children?: ReactNode;
12} 13}
13 14
14// Can this file be merged into the './loader/index.tsx' file? 15// Can this file be merged into the './loader/index.tsx' file?
15@inject('stores') 16@inject('stores')
16@observer 17@observer
17class LoaderComponent extends Component<PropsWithChildren<IProps>> { 18class LoaderComponent extends Component<IProps> {
18 static defaultProps = { 19 render(): ReactElement {
19 loaded: false, 20 const {
20 color: 'ACCENT', 21 loaded = false,
21 }; 22 color = 'ACCENT',
23 className,
24 children,
25 } = this.props;
22 26
23 render() { 27 const loaderColor =
24 const { children, loaded, className } = this.props; 28 color !== 'ACCENT' ? color : this.props.stores!.settings.app.accentColor;
25
26 const color =
27 this.props.color !== 'ACCENT'
28 ? this.props.color
29 : this.props.stores!.settings.app.accentColor;
30 29
31 return ( 30 return (
32 <Loader 31 <Loader
33 loaded={loaded} 32 loaded={loaded}
34 width={4} 33 width={4}
35 scale={0.6} 34 scale={0.6}
36 color={color} 35 color={loaderColor}
37 component="span" 36 component="span"
38 className={className} 37 className={className}
39 > 38 >
diff --git a/src/components/ui/ServiceIcon.js b/src/components/ui/ServiceIcon.tsx
index b05d791be..39a32e44d 100644
--- a/src/components/ui/ServiceIcon.js
+++ b/src/components/ui/ServiceIcon.tsx
@@ -1,9 +1,7 @@
1import { Component } from 'react'; 1import { Component, ReactNode } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 3import withStyles, { WithStylesProps } from 'react-jss';
5import classnames from 'classnames'; 4import classnames from 'classnames';
6
7import ServiceModel from '../../models/Service'; 5import ServiceModel from '../../models/Service';
8 6
9const styles = theme => ({ 7const styles = theme => ({
@@ -24,20 +22,16 @@ const styles = theme => ({
24 }, 22 },
25}); 23});
26 24
27// Should this file be converted into the coding style similar to './toggle/index.tsx'? 25interface IProps extends WithStylesProps<typeof styles> {
28class ServiceIcon extends Component { 26 service: ServiceModel;
29 static propTypes = { 27 className?: string;
30 classes: PropTypes.object.isRequired, 28}
31 service: PropTypes.instanceOf(ServiceModel).isRequired,
32 className: PropTypes.string,
33 };
34
35 static defaultProps = {
36 className: '',
37 };
38 29
39 render() { 30// TODO - [TS DEBT] Should this file be converted into the coding style similar to './toggle/index.tsx'?
40 const { classes, className, service } = this.props; 31@observer
32class ServiceIcon extends Component<IProps> {
33 render(): ReactNode {
34 const { classes, className = '', service } = this.props;
41 35
42 return ( 36 return (
43 <div className={classnames([classes.root, className])}> 37 <div className={classnames([classes.root, className])}>
@@ -55,6 +49,4 @@ class ServiceIcon extends Component {
55 } 49 }
56} 50}
57 51
58export default injectSheet(styles, { injectTheme: true })( 52export default withStyles(styles, { injectTheme: true })(ServiceIcon);
59 observer(ServiceIcon),
60);
diff --git a/src/components/ui/Slider.js b/src/components/ui/Slider.tsx
index 90f4df1c4..ed9fe9073 100644
--- a/src/components/ui/Slider.js
+++ b/src/components/ui/Slider.tsx
@@ -1,32 +1,34 @@
1import { Component } from 'react'; 1import { ChangeEvent, Component, ReactElement } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import classnames from 'classnames'; 3import classnames from 'classnames';
5import { Field } from 'mobx-react-form'; 4import { noop } from 'lodash';
6 5
7// Should this file be converted into the coding style similar to './toggle/index.tsx'? 6interface IProps {
8class Slider extends Component { 7 field: any;
9 static propTypes = { 8 className?: string;
10 field: PropTypes.instanceOf(Field).isRequired, 9 showLabel?: boolean;
11 className: PropTypes.string, 10 disabled?: boolean;
12 showLabel: PropTypes.bool, 11 type?: 'range' | 'number';
13 disabled: PropTypes.bool, 12 onSliderChange?: (e: ChangeEvent) => void;
14 }; 13}
15
16 static defaultProps = {
17 className: '',
18 showLabel: true,
19 disabled: false,
20 };
21
22 onChange(e) {
23 const { field } = this.props;
24 14
15// TODO - [TS DEBT] Should this file be converted into the coding style similar to './toggle/index.tsx'?
16@observer
17class Slider extends Component<IProps> {
18 onChange(e: ChangeEvent<HTMLInputElement>): void {
19 const { field, onSliderChange = noop } = this.props;
25 field.onChange(e); 20 field.onChange(e);
21 onSliderChange(e);
26 } 22 }
27 23
28 render() { 24 render(): ReactElement {
29 const { field, className, showLabel, disabled } = this.props; 25 const {
26 field,
27 className = '',
28 showLabel = true,
29 disabled = false,
30 type = 'range',
31 } = this.props;
30 32
31 if (field.value === '' && field.default !== '') { 33 if (field.value === '' && field.default !== '') {
32 field.value = field.default; 34 field.value = field.default;
@@ -43,7 +45,7 @@ class Slider extends Component {
43 <div className="slider-container"> 45 <div className="slider-container">
44 <input 46 <input
45 className="slider" 47 className="slider"
46 type="range" 48 type={type}
47 id={field.id} 49 id={field.id}
48 name={field.name} 50 name={field.name}
49 value={field.value} 51 value={field.value}
@@ -64,4 +66,4 @@ class Slider extends Component {
64 } 66 }
65} 67}
66 68
67export default observer(Slider); 69export default Slider;
diff --git a/src/components/ui/StatusBarTargetUrl.js b/src/components/ui/StatusBarTargetUrl.tsx
index 3e0c98c5d..7b053f410 100644
--- a/src/components/ui/StatusBarTargetUrl.js
+++ b/src/components/ui/StatusBarTargetUrl.tsx
@@ -1,24 +1,18 @@
1import { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import classnames from 'classnames'; 3import classnames from 'classnames';
5
6import Appear from './effects/Appear'; 4import Appear from './effects/Appear';
7 5
8// Should this file be converted into the coding style similar to './toggle/index.tsx'? 6interface IProps {
9class StatusBarTargetUrl extends Component { 7 className?: string;
10 static propTypes = { 8 text?: string;
11 className: PropTypes.string, 9}
12 text: PropTypes.string,
13 };
14
15 static defaultProps = {
16 className: '',
17 text: '',
18 };
19 10
11// TODO - [TS DEBT] Should this file be converted into the coding style similar to './toggle/index.tsx'?
12@observer
13class StatusBarTargetUrl extends Component<IProps> {
20 render() { 14 render() {
21 const { className, text } = this.props; 15 const { className = '', text = '' } = this.props;
22 16
23 return ( 17 return (
24 <Appear 18 <Appear
@@ -33,4 +27,4 @@ class StatusBarTargetUrl extends Component {
33 } 27 }
34} 28}
35 29
36export default observer(StatusBarTargetUrl); 30export default StatusBarTargetUrl;
diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js
deleted file mode 100644
index 20945d191..000000000
--- a/src/components/ui/WebviewLoader/index.js
+++ /dev/null
@@ -1,37 +0,0 @@
1import { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, injectIntl } from 'react-intl';
6
7import FullscreenLoader from '../FullscreenLoader';
8import styles from './styles';
9
10const messages = defineMessages({
11 loading: {
12 id: 'service.webviewLoader.loading',
13 defaultMessage: 'Loading {service}',
14 },
15});
16
17class WebviewLoader extends Component {
18 static propTypes = {
19 name: PropTypes.string.isRequired,
20 classes: PropTypes.object.isRequired,
21 };
22
23 render() {
24 const { classes, name } = this.props;
25 const { intl } = this.props;
26 return (
27 <FullscreenLoader
28 className={classes.component}
29 title={`${intl.formatMessage(messages.loading, { service: name })}`}
30 />
31 );
32 }
33}
34
35export default injectIntl(
36 injectSheet(styles, { injectTheme: true })(observer(WebviewLoader)),
37);
diff --git a/src/components/ui/WebviewLoader/index.tsx b/src/components/ui/WebviewLoader/index.tsx
new file mode 100644
index 000000000..81923b6ca
--- /dev/null
+++ b/src/components/ui/WebviewLoader/index.tsx
@@ -0,0 +1,44 @@
1import { Component, ReactElement } from 'react';
2import { observer } from 'mobx-react';
3import injectSheet, { WithStylesProps } from 'react-jss';
4import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
5import FullscreenLoader from '../FullscreenLoader';
6
7const messages = defineMessages({
8 loading: {
9 id: 'service.webviewLoader.loading',
10 defaultMessage: 'Loading {service}',
11 },
12});
13
14const styles = theme => ({
15 component: {
16 background: theme.colorWebviewLoaderBackground,
17 padding: 20,
18 width: 'auto',
19 margin: [0, 'auto'],
20 borderRadius: 6,
21 },
22});
23
24interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
25 name: string;
26 loaded?: boolean;
27}
28
29class WebviewLoader extends Component<IProps> {
30 render(): ReactElement {
31 const { classes, name, loaded = false, intl } = this.props;
32 return (
33 <FullscreenLoader
34 className={classes.component}
35 title={`${intl.formatMessage(messages.loading, { service: name })}`}
36 loaded={loaded}
37 />
38 );
39 }
40}
41
42export default injectIntl(
43 injectSheet(styles, { injectTheme: true })(observer(WebviewLoader)),
44);
diff --git a/src/components/ui/WebviewLoader/styles.ts b/src/components/ui/WebviewLoader/styles.ts
deleted file mode 100644
index dbd75db8a..000000000
--- a/src/components/ui/WebviewLoader/styles.ts
+++ /dev/null
@@ -1,9 +0,0 @@
1export default theme => ({
2 component: {
3 background: theme.colorWebviewLoaderBackground,
4 padding: 20,
5 width: 'auto',
6 margin: [0, 'auto'],
7 borderRadius: 6,
8 },
9});
diff --git a/src/features/todos/actions.ts b/src/features/todos/actions.ts
index b47a076b9..31b14d40b 100644
--- a/src/features/todos/actions.ts
+++ b/src/features/todos/actions.ts
@@ -1,17 +1,19 @@
1import { Webview } from 'react-electron-web-view';
1import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
2import { ReactElement } from 'react';
3import { createActionsFromDefinitions } from '../../actions/lib/actions'; 3import { createActionsFromDefinitions } from '../../actions/lib/actions';
4 4
5export interface TodoClientMessage {
6 action: string;
7 data: object;
8}
9
5interface TodoActionsType { 10interface TodoActionsType {
6 resize: (width: number) => void; 11 resize: (width: number) => void;
7 toggleTodosPanel: () => void; 12 toggleTodosPanel: () => void;
8 toggleTodosFeatureVisibility: () => void; 13 toggleTodosFeatureVisibility: () => void;
9 setTodosWebview: (webview: ReactElement) => void; 14 setTodosWebview: (webview: Webview) => void;
10 handleHostMessage: (action: string, data: object) => void; 15 handleHostMessage: (action: string, data: object) => void;
11 handleClientMessage: ( 16 handleClientMessage: (channel: string, message: TodoClientMessage) => void;
12 channel: string,
13 message: { action: string; data: object },
14 ) => void;
15 openDevTools: () => void; 17 openDevTools: () => void;
16 reload: () => void; 18 reload: () => void;
17} 19}
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.tsx
index 780864b91..3385ff74c 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.tsx
@@ -1,11 +1,10 @@
1import { Component } from 'react'; 1import { Component, createRef, ReactElement, MouseEvent } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 3import withStyles, { WithStylesProps } from 'react-jss';
5import Webview from 'react-electron-web-view'; 4import Webview from 'react-electron-web-view';
6import classnames from 'classnames'; 5import classnames from 'classnames';
7
8import { TODOS_PARTITION_ID } from '../../../config'; 6import { TODOS_PARTITION_ID } from '../../../config';
7import { TodoClientMessage } from '../actions';
9 8
10const styles = theme => ({ 9const styles = theme => ({
11 root: { 10 root: {
@@ -48,51 +47,71 @@ const styles = theme => ({
48 }, 47 },
49}); 48});
50 49
51class TodosWebview extends Component { 50interface IProps extends WithStylesProps<typeof styles> {
52 static propTypes = { 51 isTodosServiceActive: boolean;
53 classes: PropTypes.object.isRequired, 52 isVisible: boolean;
54 isTodosServiceActive: PropTypes.bool.isRequired, 53 handleClientMessage: (channel: string, message: TodoClientMessage) => void;
55 isVisible: PropTypes.bool.isRequired, 54 setTodosWebview: (webView: Webview) => void;
56 handleClientMessage: PropTypes.func.isRequired, 55 resize: (newWidth: number) => void;
57 setTodosWebview: PropTypes.func.isRequired, 56 width: number;
58 resize: PropTypes.func.isRequired, 57 minWidth: number;
59 width: PropTypes.number.isRequired, 58 userAgent: string;
60 minWidth: PropTypes.number.isRequired, 59 todoUrl: string;
61 userAgent: PropTypes.string.isRequired, 60 isTodoUrlValid: boolean;
62 todoUrl: PropTypes.string.isRequired, 61}
63 isTodoUrlValid: PropTypes.bool.isRequired, 62
64 }; 63interface IState {
65 64 isDragging: boolean;
66 state = { 65 width: number;
67 isDragging: false, 66 initialPos: number;
68 width: 300, 67 delta: number;
69 }; 68}
69
70@observer
71class TodosWebview extends Component<IProps, IState> {
72 private node = createRef<HTMLDivElement>();
73
74 private webview: Webview;
75
76 constructor(props: IProps) {
77 super(props);
78
79 this.state = {
80 isDragging: false,
81 width: 300,
82 initialPos: 0,
83 delta: 0,
84 };
85 this.resizePanel = this.resizePanel.bind(this);
86 this.stopResize = this.stopResize.bind(this);
87 }
70 88
71 componentDidMount() { 89 componentDidMount() {
72 this.setState({ 90 this.setState({
73 width: this.props.width, 91 width: this.props.width,
74 }); 92 });
75 93
76 this.node.addEventListener('mousemove', this.resizePanel.bind(this)); 94 if (this.node.current) {
77 this.node.addEventListener('mouseup', this.stopResize.bind(this)); 95 this.node.current.addEventListener('mousemove', this.resizePanel);
78 this.node.addEventListener('mouseleave', this.stopResize.bind(this)); 96 this.node.current.addEventListener('mouseup', this.stopResize);
97 this.node.current.addEventListener('mouseleave', this.stopResize);
98 }
79 } 99 }
80 100
81 startResize = event => { 101 startResize(e: MouseEvent<HTMLDivElement>): void {
82 this.setState({ 102 this.setState({
83 isDragging: true, 103 isDragging: true,
84 initialPos: event.clientX, 104 initialPos: e.clientX,
85 delta: 0, 105 delta: 0,
86 }); 106 });
87 }; 107 }
88 108
89 resizePanel(e) { 109 resizePanel(e: MouseEventInit): void {
90 const { minWidth } = this.props; 110 const { minWidth } = this.props;
91
92 const { isDragging, initialPos } = this.state; 111 const { isDragging, initialPos } = this.state;
93 112
94 if (isDragging && Math.abs(e.clientX - window.innerWidth) > minWidth) { 113 if (isDragging && Math.abs(e.clientX! - window.innerWidth) > minWidth) {
95 const delta = e.clientX - initialPos; 114 const delta = e.clientX! - initialPos;
96 115
97 this.setState({ 116 this.setState({
98 delta, 117 delta,
@@ -100,9 +119,8 @@ class TodosWebview extends Component {
100 } 119 }
101 } 120 }
102 121
103 stopResize() { 122 stopResize(): void {
104 const { resize, minWidth } = this.props; 123 const { resize, minWidth } = this.props;
105
106 const { isDragging, delta, width } = this.state; 124 const { isDragging, delta, width } = this.state;
107 125
108 if (isDragging) { 126 if (isDragging) {
@@ -123,14 +141,17 @@ class TodosWebview extends Component {
123 } 141 }
124 142
125 startListeningToIpcMessages() { 143 startListeningToIpcMessages() {
144 if (!this.webview) {
145 return;
146 }
147
126 const { handleClientMessage } = this.props; 148 const { handleClientMessage } = this.props;
127 if (!this.webview) return;
128 this.webview.addEventListener('ipc-message', e => { 149 this.webview.addEventListener('ipc-message', e => {
129 handleClientMessage({ channel: e.channel, message: e.args[0] }); 150 handleClientMessage(e.channel, e.args[0]);
130 }); 151 });
131 } 152 }
132 153
133 render() { 154 render(): ReactElement {
134 const { 155 const {
135 classes, 156 classes,
136 isTodosServiceActive, 157 isTodosServiceActive,
@@ -141,10 +162,9 @@ class TodosWebview extends Component {
141 } = this.props; 162 } = this.props;
142 163
143 const { width, delta, isDragging } = this.state; 164 const { width, delta, isDragging } = this.state;
144
145 let displayedWidth = isVisible ? width : 0; 165 let displayedWidth = isVisible ? width : 0;
146 if (isTodosServiceActive) { 166 if (isTodosServiceActive) {
147 displayedWidth = null; 167 displayedWidth = 0;
148 } 168 }
149 169
150 return ( 170 return (
@@ -157,9 +177,7 @@ class TodosWebview extends Component {
157 })} 177 })}
158 style={{ width: displayedWidth }} 178 style={{ width: displayedWidth }}
159 onMouseUp={() => this.stopResize()} 179 onMouseUp={() => this.stopResize()}
160 ref={node => { 180 ref={this.node}
161 this.node = node;
162 }}
163 id="todos-panel" 181 id="todos-panel"
164 > 182 >
165 <div 183 <div
@@ -168,7 +186,7 @@ class TodosWebview extends Component {
168 left: delta, 186 left: delta,
169 ...(isDragging ? { width: 600, marginLeft: -200 } : {}), 187 ...(isDragging ? { width: 600, marginLeft: -200 } : {}),
170 }} // This hack is required as resizing with webviews beneath behaves quite bad 188 }} // This hack is required as resizing with webviews beneath behaves quite bad
171 onMouseDown={e => this.startResize(e)} 189 onMouseDown={this.startResize}
172 /> 190 />
173 {isDragging && ( 191 {isDragging && (
174 <div 192 <div
@@ -178,7 +196,7 @@ class TodosWebview extends Component {
178 )} 196 )}
179 {isTodoUrlValid && ( 197 {isTodoUrlValid && (
180 <Webview 198 <Webview
181 className={classes.webview} 199 // className={classes.webview} // TODO - [TS DEBT] style not found
182 onDidAttach={() => { 200 onDidAttach={() => {
183 const { setTodosWebview } = this.props; 201 const { setTodosWebview } = this.props;
184 setTodosWebview(this.webview); 202 setTodosWebview(this.webview);
@@ -198,6 +216,4 @@ class TodosWebview extends Component {
198 } 216 }
199} 217}
200 218
201export default injectSheet(styles, { injectTheme: true })( 219export default withStyles(styles, { injectTheme: true })(TodosWebview);
202 observer(TodosWebview),
203);
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.tsx
index b97506767..17f61bd95 100644
--- a/src/features/todos/containers/TodosScreen.js
+++ b/src/features/todos/containers/TodosScreen.tsx
@@ -1,22 +1,26 @@
1import { Component } from 'react'; 1import { Component, ReactElement } from 'react';
2import { observer, inject } from 'mobx-react'; 2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4
5import FeaturesStore from '../../../stores/FeaturesStore';
6import TodosWebview from '../components/TodosWebview'; 3import TodosWebview from '../components/TodosWebview';
7import ErrorBoundary from '../../../components/util/ErrorBoundary'; 4import ErrorBoundary from '../../../components/util/ErrorBoundary';
8import { todosStore } from '..'; 5import { todosStore } from '..';
9import { TODOS_MIN_WIDTH } from '../../../config'; 6import { TODOS_MIN_WIDTH } from '../../../config';
10import { todoActions } from '../actions'; 7import { todoActions } from '../actions';
11import ServicesStore from '../../../stores/ServicesStore'; 8import { RealStores } from '../../../stores';
9
10interface IProps {
11 stores?: RealStores;
12}
12 13
13class TodosScreen extends Component { 14@inject('stores', 'actions')
14 render() { 15@observer
15 if ( 16class TodosScreen extends Component<IProps> {
17 render(): ReactElement | null {
18 const showTodoScreen =
16 !todosStore || 19 !todosStore ||
17 !todosStore.isFeatureActive || 20 !todosStore.isFeatureActive ||
18 todosStore.isTodosPanelForceHidden 21 todosStore.isTodosPanelForceHidden;
19 ) { 22
23 if (showTodoScreen) {
20 return null; 24 return null;
21 } 25 }
22 26
@@ -24,15 +28,15 @@ class TodosScreen extends Component {
24 <ErrorBoundary> 28 <ErrorBoundary>
25 <TodosWebview 29 <TodosWebview
26 isTodosServiceActive={ 30 isTodosServiceActive={
27 this.props.stores.services.isTodosServiceActive || false 31 this.props.stores!.services.isTodosServiceActive || false
28 } 32 }
29 isVisible={todosStore.isTodosPanelVisible} 33 isVisible={todosStore.isTodosPanelVisible}
30 togglePanel={todoActions.toggleTodosPanel} 34 // togglePanel={todoActions.toggleTodosPanel} // TODO - [TECH DEBT][PROP NOT USED IN COMPONENT] check it later
31 handleClientMessage={todoActions.handleClientMessage} 35 handleClientMessage={todoActions.handleClientMessage}
32 setTodosWebview={webview => todoActions.setTodosWebview({ webview })} 36 setTodosWebview={webview => todoActions.setTodosWebview(webview)}
33 width={todosStore.width} 37 width={todosStore.width}
34 minWidth={TODOS_MIN_WIDTH} 38 minWidth={TODOS_MIN_WIDTH}
35 resize={width => todoActions.resize({ width })} 39 resize={width => todoActions.resize(width)}
36 userAgent={todosStore.userAgent} 40 userAgent={todosStore.userAgent}
37 todoUrl={todosStore.todoUrl} 41 todoUrl={todosStore.todoUrl}
38 isTodoUrlValid={todosStore.isTodoUrlValid} 42 isTodoUrlValid={todosStore.isTodoUrlValid}
@@ -42,11 +46,4 @@ class TodosScreen extends Component {
42 } 46 }
43} 47}
44 48
45export default inject('stores', 'actions')(observer(TodosScreen)); 49export default TodosScreen;
46
47TodosScreen.propTypes = {
48 stores: PropTypes.shape({
49 features: PropTypes.instanceOf(FeaturesStore).isRequired,
50 services: PropTypes.instanceOf(ServicesStore).isRequired,
51 }).isRequired,
52};
diff --git a/src/features/utils/FeatureStore.js b/src/features/utils/FeatureStore.ts
index eada332d7..2bdd167f3 100644
--- a/src/features/utils/FeatureStore.js
+++ b/src/features/utils/FeatureStore.ts
@@ -1,7 +1,9 @@
1import Reaction from '../../stores/lib/Reaction';
2
1export default class FeatureStore { 3export default class FeatureStore {
2 _actions = []; 4 _actions: any = [];
3 5
4 _reactions = []; 6 _reactions: Reaction[] = [];
5 7
6 stop() { 8 stop() {
7 this._stopActions(); 9 this._stopActions();
@@ -9,32 +11,38 @@ export default class FeatureStore {
9 } 11 }
10 12
11 // ACTIONS 13 // ACTIONS
12
13 _registerActions(actions) { 14 _registerActions(actions) {
14 this._actions = actions; 15 this._actions = actions;
15 this._startActions(); 16 this._startActions();
16 } 17 }
17 18
18 _startActions(actions = this._actions) { 19 _startActions(actions = this._actions) {
19 for (const a of actions) a.start(); 20 for (const action of actions) {
21 action.start();
22 }
20 } 23 }
21 24
22 _stopActions(actions = this._actions) { 25 _stopActions(actions = this._actions) {
23 for (const a of actions) a.stop(); 26 for (const action of actions) {
27 action.stop();
28 }
24 } 29 }
25 30
26 // REACTIONS 31 // REACTIONS
27
28 _registerReactions(reactions) { 32 _registerReactions(reactions) {
29 this._reactions = reactions; 33 this._reactions = reactions;
30 this._startReactions(); 34 this._startReactions();
31 } 35 }
32 36
33 _startReactions(reactions = this._reactions) { 37 _startReactions(reactions = this._reactions) {
34 for (const r of reactions) r.start(); 38 for (const reaction of reactions) {
39 reaction.start();
40 }
35 } 41 }
36 42
37 _stopReactions(reactions = this._reactions) { 43 _stopReactions(reactions = this._reactions) {
38 for (const r of reactions) r.stop(); 44 for (const reaction of reactions) {
45 reaction.stop();
46 }
39 } 47 }
40} 48}
diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.tsx
index ff73758c1..c9af22c96 100644
--- a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.tsx
@@ -1,12 +1,11 @@
1import { Component } from 'react'; 1import { Component, ReactElement } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 3import withStyles, { WithStylesProps } from 'react-jss';
5import classnames from 'classnames'; 4import classnames from 'classnames';
6import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
7
8import Loader from '../../../components/ui/loader/index'; 6import Loader from '../../../components/ui/loader/index';
9import { workspaceStore } from '../index'; 7import { workspaceStore } from '../index';
8import { Theme } from '../../../themes';
10 9
11const messages = defineMessages({ 10const messages = defineMessages({
12 switchingTo: { 11 switchingTo: {
@@ -15,11 +14,10 @@ const messages = defineMessages({
15 }, 14 },
16}); 15});
17 16
18let wrapperTransition = 'none'; 17const wrapperTransition =
19 18 window && window.matchMedia('(prefers-reduced-motion: no-preference)')
20if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { 19 ? 'width 0.5s ease'
21 wrapperTransition = 'width 0.5s ease'; 20 : 'none';
22}
23 21
24const styles = theme => ({ 22const styles = theme => ({
25 wrapper: { 23 wrapper: {
@@ -53,26 +51,30 @@ const styles = theme => ({
53 }, 51 },
54}); 52});
55 53
56class WorkspaceSwitchingIndicator extends Component { 54interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
57 static propTypes = { 55 theme?: Theme;
58 classes: PropTypes.object.isRequired, 56}
59 theme: PropTypes.object.isRequired,
60 };
61 57
62 render() { 58@observer
63 const { classes, theme } = this.props; 59class WorkspaceSwitchingIndicator extends Component<IProps> {
64 const { intl } = this.props; 60 render(): ReactElement | null {
61 const { classes, intl, theme } = this.props;
65 const { isSwitchingWorkspace, nextWorkspace } = workspaceStore; 62 const { isSwitchingWorkspace, nextWorkspace } = workspaceStore;
66 if (!isSwitchingWorkspace) return null; 63
64 if (!isSwitchingWorkspace) {
65 return null;
66 }
67
67 const nextWorkspaceName = nextWorkspace 68 const nextWorkspaceName = nextWorkspace
68 ? nextWorkspace.name 69 ? nextWorkspace.name
69 : 'All services'; 70 : 'All services';
71
70 return ( 72 return (
71 <div className={classnames([classes.wrapper])}> 73 <div className={classnames([classes.wrapper])}>
72 <div className={classes.component}> 74 <div className={classes.component}>
73 <Loader 75 <Loader
74 className={classes.spinner} 76 className={classes.spinner}
75 color={theme.workspaces.switchingIndicator.spinnerColor} 77 color={theme?.workspaces.switchingIndicator.spinnerColor}
76 /> 78 />
77 <p className={classes.message}> 79 <p className={classes.message}>
78 {`${intl.formatMessage(messages.switchingTo)} ${nextWorkspaceName}`} 80 {`${intl.formatMessage(messages.switchingTo)} ${nextWorkspaceName}`}
@@ -84,7 +86,5 @@ class WorkspaceSwitchingIndicator extends Component {
84} 86}
85 87
86export default injectIntl( 88export default injectIntl(
87 injectSheet(styles, { injectTheme: true })( 89 withStyles(styles, { injectTheme: true })(WorkspaceSwitchingIndicator),
88 observer(WorkspaceSwitchingIndicator),
89 ),
90); 90);
diff --git a/src/helpers/url-helpers.ts b/src/helpers/url-helpers.ts
index 69a2cc4dc..9c5cf7752 100644
--- a/src/helpers/url-helpers.ts
+++ b/src/helpers/url-helpers.ts
@@ -1,14 +1,12 @@
1// This is taken from: https://benjamin-altpeter.de/shell-openexternal-dangers/ 1// This is taken from: https://benjamin-altpeter.de/shell-openexternal-dangers/
2
3import { URL } from 'url'; 2import { URL } from 'url';
4import { ensureDirSync, existsSync } from 'fs-extra'; 3import { ensureDirSync, existsSync } from 'fs-extra';
5import { shell } from 'electron'; 4import { shell } from 'electron';
6
7import { ALLOWED_PROTOCOLS } from '../config'; 5import { ALLOWED_PROTOCOLS } from '../config';
8 6
9const debug = require('../preload-safe-debug')('Ferdium:Helpers:url'); 7const debug = require('../preload-safe-debug')('Ferdium:Helpers:url');
10 8
11export function isValidExternalURL(url: string | URL) { 9export function isValidExternalURL(url: string | URL): boolean {
12 let parsedUrl: URL; 10 let parsedUrl: URL;
13 try { 11 try {
14 parsedUrl = new URL(url.toString()); 12 parsedUrl = new URL(url.toString());
@@ -17,13 +15,12 @@ export function isValidExternalURL(url: string | URL) {
17 } 15 }
18 16
19 const isAllowed = ALLOWED_PROTOCOLS.includes(parsedUrl.protocol); 17 const isAllowed = ALLOWED_PROTOCOLS.includes(parsedUrl.protocol);
20
21 debug('protocol check is', isAllowed, 'for:', url); 18 debug('protocol check is', isAllowed, 'for:', url);
22 19
23 return isAllowed; 20 return isAllowed;
24} 21}
25 22
26export function fixUrl(url: string | URL) { 23export function fixUrl(url: string | URL): string {
27 return url 24 return url
28 .toString() 25 .toString()
29 .replaceAll('//', '/') 26 .replaceAll('//', '/')
@@ -32,11 +29,11 @@ export function fixUrl(url: string | URL) {
32 .replaceAll('file:/', 'file://'); 29 .replaceAll('file:/', 'file://');
33} 30}
34 31
35export function isValidFileUrl(path: string) { 32export function isValidFileUrl(path: string): boolean {
36 return path.startsWith('file') && existsSync(new URL(path)); 33 return path.startsWith('file') && existsSync(new URL(path));
37} 34}
38 35
39export async function openPath(folderName: string) { 36export async function openPath(folderName: string): Promise<void> {
40 ensureDirSync(folderName); 37 ensureDirSync(folderName);
41 shell.openPath(folderName); 38 shell.openPath(folderName);
42} 39}
@@ -45,7 +42,7 @@ export async function openPath(folderName: string) {
45export function openExternalUrl( 42export function openExternalUrl(
46 url: string | URL, 43 url: string | URL,
47 skipValidityCheck: boolean = false, 44 skipValidityCheck: boolean = false,
48) { 45): void {
49 const fixedUrl = fixUrl(url.toString()); 46 const fixedUrl = fixUrl(url.toString());
50 debug('Open url:', fixedUrl, 'with skipValidityCheck:', skipValidityCheck); 47 debug('Open url:', fixedUrl, 'with skipValidityCheck:', skipValidityCheck);
51 if (skipValidityCheck || isValidExternalURL(fixedUrl)) { 48 if (skipValidityCheck || isValidExternalURL(fixedUrl)) {
diff --git a/src/stores/FeaturesStore.ts b/src/stores/FeaturesStore.ts
index 5f43ccf84..8584b6060 100644
--- a/src/stores/FeaturesStore.ts
+++ b/src/stores/FeaturesStore.ts
@@ -5,7 +5,6 @@ import {
5 observable, 5 observable,
6 runInAction, 6 runInAction,
7} from 'mobx'; 7} from 'mobx';
8
9import { Stores } from '../@types/stores.types'; 8import { Stores } from '../@types/stores.types';
10import { ApiInterface } from '../api'; 9import { ApiInterface } from '../api';
11import { Actions } from '../actions/lib/actions'; 10import { Actions } from '../actions/lib/actions';
@@ -21,6 +20,14 @@ import appearance from '../features/appearance';
21import TypedStore from './lib/TypedStore'; 20import TypedStore from './lib/TypedStore';
22 21
23export default class FeaturesStore extends TypedStore { 22export default class FeaturesStore extends TypedStore {
23 @observable features = {};
24
25 constructor(stores: Stores, api: ApiInterface, actions: Actions) {
26 super(stores, api, actions);
27
28 makeObservable(this);
29 }
30
24 @observable defaultFeaturesRequest = new CachedRequest( 31 @observable defaultFeaturesRequest = new CachedRequest(
25 this.api.features, 32 this.api.features,
26 'default', 33 'default',
@@ -31,14 +38,6 @@ export default class FeaturesStore extends TypedStore {
31 'features', 38 'features',
32 ); 39 );
33 40
34 @observable features = {};
35
36 constructor(stores: Stores, api: ApiInterface, actions: Actions) {
37 super(stores, api, actions);
38
39 makeObservable(this);
40 }
41
42 async setup(): Promise<void> { 41 async setup(): Promise<void> {
43 this.registerReactions([ 42 this.registerReactions([
44 this._updateFeatures, 43 this._updateFeatures,