aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/@types/stores.types.ts4
-rw-r--r--src/components/auth/AuthLayout.tsx8
-rw-r--r--src/features/workspaces/components/CreateWorkspaceForm.tsx (renamed from src/features/workspaces/components/CreateWorkspaceForm.js)54
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.tsx (renamed from src/features/workspaces/components/EditWorkspaceForm.js)67
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.tsx7
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.tsx (renamed from src/features/workspaces/components/WorkspaceDrawerItem.js)64
-rw-r--r--src/features/workspaces/components/WorkspaceItem.tsx28
-rw-r--r--src/features/workspaces/components/WorkspaceServiceListItem.tsx5
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.tsx (renamed from src/features/workspaces/components/WorkspacesDashboard.js)54
-rw-r--r--src/features/workspaces/containers/WorkspacesScreen.tsx13
-rw-r--r--src/stores/AppStore.ts2
-rw-r--r--src/stores/FeaturesStore.ts2
-rw-r--r--src/stores/GlobalErrorStore.ts27
-rw-r--r--src/stores/RecipesStore.ts10
-rw-r--r--src/stores/RequestStore.ts3
-rw-r--r--src/stores/ServicesStore.ts10
-rw-r--r--src/stores/UserStore.ts17
-rw-r--r--src/stores/lib/CachedRequest.ts (renamed from src/stores/lib/CachedRequest.js)49
-rw-r--r--src/stores/lib/Request.ts (renamed from src/stores/lib/Request.js)106
19 files changed, 270 insertions, 260 deletions
diff --git a/src/@types/stores.types.ts b/src/@types/stores.types.ts
index bbb37cff9..edea41ea9 100644
--- a/src/@types/stores.types.ts
+++ b/src/@types/stores.types.ts
@@ -2,8 +2,8 @@ import Workspace from '../features/workspaces/models/Workspace';
2import Recipe from '../models/Recipe'; 2import Recipe from '../models/Recipe';
3import Service from '../models/Service'; 3import Service from '../models/Service';
4import User from '../models/User'; 4import User from '../models/User';
5import { Request } from '../stores/lib/Request'; 5import Request from '../stores/lib/Request';
6import { CachedRequest } from '../stores/lib/CachedRequest'; 6import CachedRequest from '../stores/lib/CachedRequest';
7import Reaction from '../stores/lib/Reaction'; 7import Reaction from '../stores/lib/Reaction';
8 8
9// TODO: This file will be removed in the future when all stores are 9// TODO: This file will be removed in the future when all stores are
diff --git a/src/components/auth/AuthLayout.tsx b/src/components/auth/AuthLayout.tsx
index 527c2bb74..eeb93b83b 100644
--- a/src/components/auth/AuthLayout.tsx
+++ b/src/components/auth/AuthLayout.tsx
@@ -8,7 +8,7 @@ import { observer } from 'mobx-react';
8import { TitleBar } from 'electron-react-titlebar/renderer'; 8import { TitleBar } from 'electron-react-titlebar/renderer';
9import { injectIntl, WrappedComponentProps } from 'react-intl'; 9import { injectIntl, WrappedComponentProps } from 'react-intl';
10import { mdiFlash } from '@mdi/js'; 10import { mdiFlash } from '@mdi/js';
11import { GlobalError } from '../../@types/ferdium-components.types'; 11import { Response } from 'electron';
12import Link from '../ui/Link'; 12import Link from '../ui/Link';
13import InfoBar from '../ui/InfoBar'; 13import InfoBar from '../ui/InfoBar';
14import { Component as PublishDebugInfo } from '../../features/publishDebugInfo'; 14import { Component as PublishDebugInfo } from '../../features/publishDebugInfo';
@@ -22,7 +22,7 @@ import { serverName } from '../../api/apiBase';
22 22
23export interface IProps extends WrappedComponentProps { 23export interface IProps extends WrappedComponentProps {
24 children: ReactElement; 24 children: ReactElement;
25 error: GlobalError; 25 error: Response;
26 isOnline: boolean; 26 isOnline: boolean;
27 isAPIHealthy: boolean; 27 isAPIHealthy: boolean;
28 retryHealthCheck: MouseEventHandler<HTMLButtonElement>; 28 retryHealthCheck: MouseEventHandler<HTMLButtonElement>;
@@ -106,9 +106,7 @@ class AuthLayout extends Component<IProps, IState> {
106 )} 106 )}
107 <div className="auth__layout"> 107 <div className="auth__layout">
108 {/* Inject globalError into children */} 108 {/* Inject globalError into children */}
109 {cloneElement(children, { 109 {cloneElement(children, { error })}
110 error,
111 })}
112 </div> 110 </div>
113 {/* </div> */} 111 {/* </div> */}
114 <Link 112 <Link
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.tsx
index fac84bed0..eafe9f36a 100644
--- a/src/features/workspaces/components/CreateWorkspaceForm.js
+++ b/src/features/workspaces/components/CreateWorkspaceForm.tsx
@@ -1,9 +1,7 @@
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 injectSheet from 'react-jss'; 4import withStyles, { WithStylesProps } from 'react-jss';
6
7import Input from '../../../components/ui/input/index'; 5import Input from '../../../components/ui/input/index';
8import Button from '../../../components/ui/button'; 6import Button from '../../../components/ui/button';
9import Form from '../../../lib/Form'; 7import Form from '../../../lib/Form';
@@ -34,47 +32,49 @@ const styles = {
34 }, 32 },
35}; 33};
36 34
37class CreateWorkspaceForm extends Component { 35interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
38 static propTypes = { 36 isSubmitting: boolean;
39 classes: PropTypes.object.isRequired, 37 onSubmit: (...args: any[]) => void;
40 isSubmitting: PropTypes.bool.isRequired, 38}
41 onSubmit: PropTypes.func.isRequired, 39
42 }; 40@observer
41class CreateWorkspaceForm extends Component<IProps> {
42 form: Form;
43 43
44 form = (() => { 44 constructor(props: IProps) {
45 const { intl } = this.props; 45 super(props);
46 return new Form({ 46
47 this.form = new Form({
47 fields: { 48 fields: {
48 name: { 49 name: {
49 label: intl.formatMessage(messages.name), 50 label: this.props.intl.formatMessage(messages.name),
50 placeholder: intl.formatMessage(messages.name), 51 placeholder: this.props.intl.formatMessage(messages.name),
51 value: '', 52 value: '',
52 validators: [required], 53 validators: [required],
53 }, 54 },
54 }, 55 },
55 }); 56 });
56 })(); 57 }
57 58
58 submitForm() { 59 submitForm(): void {
59 const { form } = this; 60 this.form.submit({
60 form.submit({ 61 onSuccess: async form => {
61 onSuccess: async f => {
62 const { onSubmit } = this.props; 62 const { onSubmit } = this.props;
63 const values = f.values(); 63 const values = form.values();
64 onSubmit(values); 64 onSubmit(values);
65 }, 65 },
66 }); 66 });
67 } 67 }
68 68
69 render() { 69 render(): ReactElement {
70 const { intl } = this.props; 70 const { classes, isSubmitting, intl } = this.props;
71 const { classes, isSubmitting } = this.props;
72 const { form } = this; 71 const { form } = this;
72
73 return ( 73 return (
74 <div className={classes.form}> 74 <div className={classes.form}>
75 <Input 75 <Input
76 className={classes.input}
77 {...form.$('name').bind()} 76 {...form.$('name').bind()}
77 className={classes.input}
78 showLabel={false} 78 showLabel={false}
79 onEnterKey={this.submitForm.bind(this, form)} 79 onEnterKey={this.submitForm.bind(this, form)}
80 focus={workspaceStore.isUserAllowedToUseFeature} 80 focus={workspaceStore.isUserAllowedToUseFeature}
@@ -93,5 +93,5 @@ class CreateWorkspaceForm extends Component {
93} 93}
94 94
95export default injectIntl( 95export default injectIntl(
96 injectSheet(styles, { injectTheme: true })(observer(CreateWorkspaceForm)), 96 withStyles(styles, { injectTheme: true })(CreateWorkspaceForm),
97); 97);
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.tsx
index ff4e71260..a860ac2e8 100644
--- a/src/features/workspaces/components/EditWorkspaceForm.js
+++ b/src/features/workspaces/components/EditWorkspaceForm.tsx
@@ -1,11 +1,10 @@
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 { Link } from 'react-router-dom'; 4import { Link } from 'react-router-dom';
6import injectSheet from 'react-jss'; 5import withStyles, { WithStylesProps } from 'react-jss';
7import Infobox from '../../../components/ui/infobox/index'; 6import Infobox from '../../../components/ui/infobox/index';
8import Input from '../../../components/ui/input/index'; 7import Input from '../../../components/ui/input';
9import Button from '../../../components/ui/button'; 8import Button from '../../../components/ui/button';
10import Workspace from '../models/Workspace'; 9import Workspace from '../models/Workspace';
11import Service from '../../../models/Service'; 10import Service from '../../../models/Service';
@@ -69,30 +68,36 @@ const styles = {
69 }, 68 },
70}; 69};
71 70
72class EditWorkspaceForm extends Component { 71interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
73 static propTypes = { 72 onDelete: () => void;
74 classes: PropTypes.object.isRequired, 73 onSave: (...args: any[]) => void;
75 onDelete: PropTypes.func.isRequired, 74 services: Service[];
76 onSave: PropTypes.func.isRequired, 75 workspace: Workspace;
77 services: PropTypes.arrayOf(PropTypes.instanceOf(Service)).isRequired, 76 updateWorkspaceRequest: Request;
78 workspace: PropTypes.instanceOf(Workspace).isRequired, 77 deleteWorkspaceRequest: Request;
79 updateWorkspaceRequest: PropTypes.instanceOf(Request).isRequired, 78}
80 deleteWorkspaceRequest: PropTypes.instanceOf(Request).isRequired, 79
81 }; 80@observer
81class EditWorkspaceForm extends Component<IProps> {
82 form: Form;
83
84 constructor(props: IProps) {
85 super(props);
82 86
83 form = this.prepareWorkspaceForm(this.props.workspace); 87 this.form = this.prepareWorkspaceForm(this.props.workspace);
88 }
84 89
85 // eslint-disable-next-line react/no-deprecated 90 UNSAFE_componentWillReceiveProps(nextProps): void {
86 componentWillReceiveProps(nextProps) {
87 const { workspace } = this.props; 91 const { workspace } = this.props;
88 if (workspace.id !== nextProps.workspace.id) { 92 if (workspace.id !== nextProps.workspace.id) {
89 this.form = this.prepareWorkspaceForm(nextProps.workspace); 93 this.form = this.prepareWorkspaceForm(nextProps.workspace);
90 } 94 }
91 } 95 }
92 96
93 prepareWorkspaceForm(workspace) { 97 prepareWorkspaceForm(workspace: Workspace): Form {
94 const { intl, updateWorkspaceRequest } = this.props; 98 const { intl, updateWorkspaceRequest } = this.props;
95 updateWorkspaceRequest.reset(); 99 updateWorkspaceRequest.reset();
100
96 return new Form({ 101 return new Form({
97 fields: { 102 fields: {
98 name: { 103 name: {
@@ -105,6 +110,7 @@ class EditWorkspaceForm extends Component {
105 label: intl.formatMessage(messages.keepLoaded), 110 label: intl.formatMessage(messages.keepLoaded),
106 value: workspace.services.includes(KEEP_WS_LOADED_USID), 111 value: workspace.services.includes(KEEP_WS_LOADED_USID),
107 default: false, 112 default: false,
113 type: 'checkbox',
108 }, 114 },
109 services: { 115 services: {
110 value: [...workspace.services], 116 value: [...workspace.services],
@@ -113,7 +119,7 @@ class EditWorkspaceForm extends Component {
113 }); 119 });
114 } 120 }
115 121
116 save(form) { 122 save(form): void {
117 this.props.updateWorkspaceRequest.reset(); 123 this.props.updateWorkspaceRequest.reset();
118 form.submit({ 124 form.submit({
119 onSuccess: async f => { 125 onSuccess: async f => {
@@ -125,12 +131,12 @@ class EditWorkspaceForm extends Component {
125 }); 131 });
126 } 132 }
127 133
128 delete() { 134 delete(): void {
129 const { onDelete } = this.props; 135 const { onDelete } = this.props;
130 onDelete(); 136 onDelete();
131 } 137 }
132 138
133 toggleService(service) { 139 toggleService(service: Service): void {
134 const servicesField = this.form.$('services'); 140 const servicesField = this.form.$('services');
135 const serviceIds = servicesField.value; 141 const serviceIds = servicesField.value;
136 if (serviceIds.includes(service.id)) { 142 if (serviceIds.includes(service.id)) {
@@ -141,19 +147,20 @@ class EditWorkspaceForm extends Component {
141 servicesField.set(serviceIds); 147 servicesField.set(serviceIds);
142 } 148 }
143 149
144 render() { 150 render(): ReactElement {
145 const { intl } = this.props;
146 const { 151 const {
147 classes, 152 classes,
148 workspace, 153 workspace,
149 services, 154 services,
150 deleteWorkspaceRequest, 155 deleteWorkspaceRequest,
151 updateWorkspaceRequest, 156 updateWorkspaceRequest,
157 intl,
152 } = this.props; 158 } = this.props;
153 const { form } = this; 159 const { form } = this;
154 const workspaceServices = form.$('services').value; 160 const workspaceServices = form.$('services').value;
155 const isDeleting = deleteWorkspaceRequest.isExecuting; 161 const isDeleting = deleteWorkspaceRequest.isExecuting;
156 const isSaving = updateWorkspaceRequest.isExecuting; 162 const isSaving = updateWorkspaceRequest.isExecuting;
163
157 return ( 164 return (
158 <div className="settings__main"> 165 <div className="settings__main">
159 <div className="settings__header"> 166 <div className="settings__header">
@@ -195,12 +202,12 @@ class EditWorkspaceForm extends Component {
195 </div> 202 </div>
196 ) : ( 203 ) : (
197 <> 204 <>
198 {services.map(s => ( 205 {services.map(service => (
199 <WorkspaceServiceListItem 206 <WorkspaceServiceListItem
200 key={s.id} 207 key={service.id}
201 service={s} 208 service={service}
202 isInWorkspace={workspaceServices.includes(s.id)} 209 isInWorkspace={workspaceServices.includes(service.id)}
203 onToggle={() => this.toggleService(s)} 210 onToggle={() => this.toggleService(service)}
204 /> 211 />
205 ))} 212 ))}
206 </> 213 </>
@@ -236,5 +243,5 @@ class EditWorkspaceForm extends Component {
236} 243}
237 244
238export default injectIntl( 245export default injectIntl(
239 injectSheet(styles, { injectTheme: true })(observer(EditWorkspaceForm)), 246 withStyles(styles, { injectTheme: true })(EditWorkspaceForm),
240); 247);
diff --git a/src/features/workspaces/components/WorkspaceDrawer.tsx b/src/features/workspaces/components/WorkspaceDrawer.tsx
index bdbebdb0a..61284d81a 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.tsx
+++ b/src/features/workspaces/components/WorkspaceDrawer.tsx
@@ -11,7 +11,6 @@ import WorkspaceDrawerItem from './WorkspaceDrawerItem';
11import workspaceActions from '../actions'; 11import workspaceActions from '../actions';
12import { workspaceStore } from '../index'; 12import { workspaceStore } from '../index';
13import { getUserWorkspacesRequest } from '../api'; 13import { getUserWorkspacesRequest } from '../api';
14import Service from '../../../models/Service';
15import Workspace from '../models/Workspace'; 14import Workspace from '../models/Workspace';
16 15
17const messages = defineMessages({ 16const messages = defineMessages({
@@ -90,7 +89,7 @@ const styles = theme => ({
90}); 89});
91 90
92interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps { 91interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
93 getServicesForWorkspace: (workspace: Workspace | null) => Service[]; 92 getServicesForWorkspace: (workspace: Workspace | null) => string[];
94} 93}
95 94
96@observer 95@observer
@@ -150,7 +149,9 @@ class WorkspaceDrawer extends Component<IProps> {
150 name={workspace.name} 149 name={workspace.name}
151 isActive={actualWorkspace === workspace} 150 isActive={actualWorkspace === workspace}
152 onClick={() => { 151 onClick={() => {
153 if (actualWorkspace === workspace) return; 152 if (actualWorkspace === workspace) {
153 return;
154 }
154 workspaceActions.activate({ workspace }); 155 workspaceActions.activate({ workspace });
155 workspaceActions.toggleWorkspaceDrawer(); 156 workspaceActions.toggleWorkspaceDrawer();
156 }} 157 }}
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.tsx
index 22c0a39d9..0ad56d1ae 100644
--- a/src/features/workspaces/components/WorkspaceDrawerItem.js
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.tsx
@@ -1,10 +1,11 @@
1import { Menu } from '@electron/remote'; 1import { Component, MouseEventHandler, ReactElement } from 'react';
2import { Component } from 'react';
3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
5import injectSheet from 'react-jss'; 3import withStyles, { WithStylesProps } from 'react-jss';
6import classnames from 'classnames'; 4import classnames from 'classnames';
7import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
6import { noop } from 'lodash';
7import { Menu } from '@electron/remote';
8import { MenuItemConstructorOptions } from 'electron';
8import { altKey, cmdOrCtrlShortcutKey } from '../../../environment'; 9import { altKey, cmdOrCtrlShortcutKey } from '../../../environment';
9 10
10const messages = defineMessages({ 11const messages = defineMessages({
@@ -18,11 +19,10 @@ const messages = defineMessages({
18 }, 19 },
19}); 20});
20 21
21let itemTransition = 'none'; 22const itemTransition =
22 23 window && window.matchMedia('(prefers-reduced-motion: no-preference)')
23if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { 24 ? 'background-color 300ms ease-out'
24 itemTransition = 'background-color 300ms ease-out'; 25 : 'none';
25}
26 26
27const styles = theme => ({ 27const styles = theme => ({
28 item: { 28 item: {
@@ -65,35 +65,30 @@ const styles = theme => ({
65 }, 65 },
66}); 66});
67 67
68class WorkspaceDrawerItem extends Component { 68interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
69 static propTypes = { 69 isActive: boolean;
70 classes: PropTypes.object.isRequired, 70 name: string;
71 isActive: PropTypes.bool.isRequired, 71 onClick: MouseEventHandler<HTMLInputElement>;
72 name: PropTypes.string.isRequired, 72 services: string[];
73 onClick: PropTypes.func.isRequired, 73 onContextMenuEditClick?: () => void | null;
74 services: PropTypes.arrayOf(PropTypes.string).isRequired, 74 shortcutIndex: number;
75 onContextMenuEditClick: PropTypes.func, 75}
76 shortcutIndex: PropTypes.number.isRequired,
77 };
78
79 static defaultProps = {
80 onContextMenuEditClick: null,
81 };
82 76
83 render() { 77@observer
78class WorkspaceDrawerItem extends Component<IProps> {
79 render(): ReactElement {
84 const { 80 const {
85 classes, 81 classes,
86 isActive, 82 isActive,
87 name, 83 name,
88 onClick, 84 onClick,
89 onContextMenuEditClick, 85 onContextMenuEditClick = null,
90 services, 86 services,
91 shortcutIndex, 87 shortcutIndex,
88 intl,
92 } = this.props; 89 } = this.props;
93 90
94 const { intl } = this.props; 91 const contextMenuTemplate: MenuItemConstructorOptions[] = [
95
96 const contextMenuTemplate = [
97 { 92 {
98 label: name, 93 label: name,
99 enabled: false, 94 enabled: false,
@@ -103,7 +98,7 @@ class WorkspaceDrawerItem extends Component {
103 }, 98 },
104 { 99 {
105 label: intl.formatMessage(messages.contextMenuEdit), 100 label: intl.formatMessage(messages.contextMenuEdit),
106 click: onContextMenuEditClick, 101 click: onContextMenuEditClick || noop,
107 }, 102 },
108 ]; 103 ];
109 104
@@ -116,7 +111,12 @@ class WorkspaceDrawerItem extends Component {
116 isActive ? classes.isActiveItem : null, 111 isActive ? classes.isActiveItem : null,
117 ])} 112 ])}
118 onClick={onClick} 113 onClick={onClick}
119 onContextMenu={() => onContextMenuEditClick && contextMenu.popup()} 114 onContextMenu={() => {
115 if (onContextMenuEditClick) {
116 contextMenu.popup();
117 }
118 }}
119 onKeyDown={noop}
120 data-tip={`${ 120 data-tip={`${
121 shortcutIndex <= 9 121 shortcutIndex <= 9
122 ? `(${cmdOrCtrlShortcutKey(false)}+${altKey( 122 ? `(${cmdOrCtrlShortcutKey(false)}+${altKey(
@@ -149,5 +149,5 @@ class WorkspaceDrawerItem extends Component {
149} 149}
150 150
151export default injectIntl( 151export default injectIntl(
152 injectSheet(styles, { injectTheme: true })(observer(WorkspaceDrawerItem)), 152 withStyles(styles, { injectTheme: true })(WorkspaceDrawerItem),
153); 153);
diff --git a/src/features/workspaces/components/WorkspaceItem.tsx b/src/features/workspaces/components/WorkspaceItem.tsx
index eb33a0376..b097a8298 100644
--- a/src/features/workspaces/components/WorkspaceItem.tsx
+++ b/src/features/workspaces/components/WorkspaceItem.tsx
@@ -1,6 +1,8 @@
1import { Component } from 'react'; 1/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ // TODO - [TS DEBT] Need to check and remove it
2import { Component, ReactElement } from 'react';
2import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
3import injectSheet from 'react-jss'; 4import withStyles, { WithStylesProps } from 'react-jss';
5import { noop } from 'lodash';
4import Workspace from '../models/Workspace'; 6import Workspace from '../models/Workspace';
5 7
6const styles = theme => ({ 8const styles = theme => ({
@@ -14,24 +16,24 @@ const styles = theme => ({
14 columnName: {}, 16 columnName: {},
15}); 17});
16 18
17type Props = { 19interface IProps extends WithStylesProps<typeof styles> {
18 classes: any; 20 workspace: Workspace;
19 workspace: typeof Workspace; 21 onItemClick: (workspace: Workspace) => void;
20 onItemClick: (workspace) => void; 22}
21};
22 23
23class WorkspaceItem extends Component<Props> { 24@observer
24 render() { 25class WorkspaceItem extends Component<IProps> {
26 render(): ReactElement {
25 const { classes, workspace, onItemClick } = this.props; 27 const { classes, workspace, onItemClick } = this.props;
26 28
27 return ( 29 return (
28 <tr className={classes.row}> 30 <tr className={classes.row}>
29 <td onClick={() => onItemClick(workspace)}>{workspace.name}</td> 31 <td onClick={() => onItemClick(workspace)} onKeyDown={noop}>
32 {workspace.name}
33 </td>
30 </tr> 34 </tr>
31 ); 35 );
32 } 36 }
33} 37}
34 38
35export default injectSheet(styles, { injectTheme: true })( 39export default withStyles(styles, { injectTheme: true })(WorkspaceItem);
36 observer(WorkspaceItem),
37);
diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.tsx b/src/features/workspaces/components/WorkspaceServiceListItem.tsx
index e708d5cdf..9034be37c 100644
--- a/src/features/workspaces/components/WorkspaceServiceListItem.tsx
+++ b/src/features/workspaces/components/WorkspaceServiceListItem.tsx
@@ -2,6 +2,7 @@ import { Component, ReactElement } from 'react';
2import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
3import withStyles, { WithStylesProps } from 'react-jss'; 3import withStyles, { WithStylesProps } from 'react-jss';
4import classnames from 'classnames'; 4import classnames from 'classnames';
5import { noop } from 'lodash';
5import Toggle from '../../../components/ui/toggle'; 6import Toggle from '../../../components/ui/toggle';
6import ServiceIcon from '../../../components/ui/ServiceIcon'; 7import ServiceIcon from '../../../components/ui/ServiceIcon';
7import Service from '../../../models/Service'; 8import Service from '../../../models/Service';
@@ -39,9 +40,9 @@ interface IProps extends WithStylesProps<typeof styles> {
39class WorkspaceServiceListItem extends Component<IProps> { 40class WorkspaceServiceListItem extends Component<IProps> {
40 render(): ReactElement { 41 render(): ReactElement {
41 const { classes, isInWorkspace, onToggle, service } = this.props; 42 const { classes, isInWorkspace, onToggle, service } = this.props;
42
43 return ( 43 return (
44 <div className={classes.listItem}> 44 // onclick in below div used to fix bug raised under toggle duplicate component removal
45 <div className={classes.listItem} onClick={onToggle} onKeyDown={noop}>
45 <ServiceIcon className={classes.serviceIcon} service={service} /> 46 <ServiceIcon className={classes.serviceIcon} service={service} />
46 <span 47 <span
47 className={classnames([ 48 className={classnames([
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.tsx
index 87ba06a2d..60fc7a0ce 100644
--- a/src/features/workspaces/components/WorkspacesDashboard.js
+++ b/src/features/workspaces/components/WorkspacesDashboard.tsx
@@ -1,18 +1,16 @@
1/* eslint-disable react/jsx-no-useless-fragment */ 1/* eslint-disable react/jsx-no-useless-fragment */
2import { Component } from 'react'; 2import { Component, ReactElement } from 'react';
3import PropTypes from 'prop-types'; 3import { observer } from 'mobx-react';
4import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; 4import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
5import { defineMessages, injectIntl } from 'react-intl'; 5import withStyles, { WithStylesProps } from 'react-jss';
6import injectSheet from 'react-jss';
7
8import Infobox from '../../../components/ui/infobox/index'; 6import Infobox from '../../../components/ui/infobox/index';
9import Loader from '../../../components/ui/Loader'; 7import Loader from '../../../components/ui/Loader';
10import WorkspaceItem from './WorkspaceItem'; 8import WorkspaceItem from './WorkspaceItem';
11import CreateWorkspaceForm from './CreateWorkspaceForm'; 9import CreateWorkspaceForm from './CreateWorkspaceForm';
12import Request from '../../../stores/lib/Request'; 10import Request from '../../../stores/lib/Request';
13import Appear from '../../../components/ui/effects/Appear'; 11import Appear from '../../../components/ui/effects/Appear';
14import UIStore from '../../../stores/UIStore';
15import { H1 } from '../../../components/ui/headline'; 12import { H1 } from '../../../components/ui/headline';
13import Workspace from '../models/Workspace';
16 14
17const messages = defineMessages({ 15const messages = defineMessages({
18 headline: { 16 headline: {
@@ -70,19 +68,19 @@ const styles = {
70 }, 68 },
71}; 69};
72 70
73class WorkspacesDashboard extends Component { 71interface IProps extends WithStylesProps<typeof styles>, WrappedComponentProps {
74 static propTypes = { 72 getUserWorkspacesRequest: Request;
75 classes: PropTypes.object.isRequired, 73 createWorkspaceRequest: Request;
76 getUserWorkspacesRequest: PropTypes.instanceOf(Request).isRequired, 74 deleteWorkspaceRequest: Request;
77 createWorkspaceRequest: PropTypes.instanceOf(Request).isRequired, 75 updateWorkspaceRequest: Request;
78 deleteWorkspaceRequest: PropTypes.instanceOf(Request).isRequired, 76 onCreateWorkspaceSubmit: (workspace: Workspace) => void;
79 updateWorkspaceRequest: PropTypes.instanceOf(Request).isRequired, 77 onWorkspaceClick: (workspace: Workspace) => void;
80 onCreateWorkspaceSubmit: PropTypes.func.isRequired, 78 workspaces: Workspace[];
81 onWorkspaceClick: PropTypes.func.isRequired, 79}
82 workspaces: MobxPropTypes.arrayOrObservableArray.isRequired,
83 };
84 80
85 render() { 81@observer
82class WorkspacesDashboard extends Component<IProps> {
83 render(): ReactElement {
86 const { 84 const {
87 classes, 85 classes,
88 getUserWorkspacesRequest, 86 getUserWorkspacesRequest,
@@ -108,7 +106,7 @@ class WorkspacesDashboard extends Component {
108 <Infobox 106 <Infobox
109 type="success" 107 type="success"
110 icon="checkbox-marked-circle-outline" 108 icon="checkbox-marked-circle-outline"
111 dismissable 109 dismissible
112 onUnmount={updateWorkspaceRequest.reset} 110 onUnmount={updateWorkspaceRequest.reset}
113 > 111 >
114 {intl.formatMessage(messages.updatedInfo)} 112 {intl.formatMessage(messages.updatedInfo)}
@@ -122,7 +120,7 @@ class WorkspacesDashboard extends Component {
122 <Infobox 120 <Infobox
123 type="success" 121 type="success"
124 icon="checkbox-marked-circle-outline" 122 icon="checkbox-marked-circle-outline"
125 dismissable 123 dismissible
126 onUnmount={deleteWorkspaceRequest.reset} 124 onUnmount={deleteWorkspaceRequest.reset}
127 > 125 >
128 {intl.formatMessage(messages.deletedInfo)} 126 {intl.formatMessage(messages.deletedInfo)}
@@ -147,7 +145,7 @@ class WorkspacesDashboard extends Component {
147 icon="alert" 145 icon="alert"
148 type="danger" 146 type="danger"
149 ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)} 147 ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)}
150 ctaLoading={getUserWorkspacesRequest.isExecuting} 148 // ctaLoading={getUserWorkspacesRequest.isExecuting} // TODO - [TECH DEBT][PROP NOT USED IN COMPONENT] need to check and update
151 ctaOnClick={getUserWorkspacesRequest.retry} 149 ctaOnClick={getUserWorkspacesRequest.retry}
152 > 150 >
153 {intl.formatMessage(messages.workspacesRequestFailed)} 151 {intl.formatMessage(messages.workspacesRequestFailed)}
@@ -165,7 +163,7 @@ class WorkspacesDashboard extends Component {
165 </p> 163 </p>
166 </div> 164 </div>
167 ) : ( 165 ) : (
168 <table className={classes.table}> 166 <table className={classes.table} role="grid">
169 {/* ===== Workspaces list ===== */} 167 {/* ===== Workspaces list ===== */}
170 <tbody> 168 <tbody>
171 {workspaces.map(workspace => ( 169 {workspaces.map(workspace => (
@@ -189,13 +187,5 @@ class WorkspacesDashboard extends Component {
189} 187}
190 188
191export default injectIntl( 189export default injectIntl(
192 inject('stores')( 190 withStyles(styles, { injectTheme: true })(WorkspacesDashboard),
193 injectSheet(styles, { injectTheme: true })(observer(WorkspacesDashboard)),
194 ),
195); 191);
196
197WorkspacesDashboard.propTypes = {
198 stores: PropTypes.shape({
199 ui: PropTypes.instanceOf(UIStore).isRequired,
200 }).isRequired,
201};
diff --git a/src/features/workspaces/containers/WorkspacesScreen.tsx b/src/features/workspaces/containers/WorkspacesScreen.tsx
index d43dc5efa..39f19935f 100644
--- a/src/features/workspaces/containers/WorkspacesScreen.tsx
+++ b/src/features/workspaces/containers/WorkspacesScreen.tsx
@@ -10,8 +10,13 @@ import {
10 getUserWorkspacesRequest, 10 getUserWorkspacesRequest,
11 updateWorkspaceRequest, 11 updateWorkspaceRequest,
12} from '../api'; 12} from '../api';
13import Workspace from '../models/Workspace';
13 14
14class WorkspacesScreen extends Component<StoresProps> { 15interface IProps extends StoresProps {}
16
17@inject('stores', 'actions')
18@observer
19class WorkspacesScreen extends Component<IProps> {
15 render() { 20 render() {
16 const { actions } = this.props; 21 const { actions } = this.props;
17 return ( 22 return (
@@ -23,11 +28,13 @@ class WorkspacesScreen extends Component<StoresProps> {
23 deleteWorkspaceRequest={deleteWorkspaceRequest} 28 deleteWorkspaceRequest={deleteWorkspaceRequest}
24 updateWorkspaceRequest={updateWorkspaceRequest} 29 updateWorkspaceRequest={updateWorkspaceRequest}
25 onCreateWorkspaceSubmit={data => actions.workspaces.create(data)} 30 onCreateWorkspaceSubmit={data => actions.workspaces.create(data)}
26 onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })} 31 onWorkspaceClick={(workspace: Workspace) =>
32 actions.workspaces.edit({ workspace })
33 }
27 /> 34 />
28 </ErrorBoundary> 35 </ErrorBoundary>
29 ); 36 );
30 } 37 }
31} 38}
32 39
33export default inject('stores', 'actions')(observer(WorkspacesScreen)); 40export default WorkspacesScreen;
diff --git a/src/stores/AppStore.ts b/src/stores/AppStore.ts
index aab279e59..2db90bfa0 100644
--- a/src/stores/AppStore.ts
+++ b/src/stores/AppStore.ts
@@ -494,7 +494,7 @@ export default class AppStore extends TypedStore {
494 ), 494 ),
495 ); 495 );
496 496
497 await clearAppCache._promise; 497 await clearAppCache.promise;
498 498
499 await sleep(ms('1s')); 499 await sleep(ms('1s'));
500 500
diff --git a/src/stores/FeaturesStore.ts b/src/stores/FeaturesStore.ts
index ed0c6c17b..5f43ccf84 100644
--- a/src/stores/FeaturesStore.ts
+++ b/src/stores/FeaturesStore.ts
@@ -45,7 +45,7 @@ export default class FeaturesStore extends TypedStore {
45 this._monitorLoginStatus.bind(this), 45 this._monitorLoginStatus.bind(this),
46 ]); 46 ]);
47 47
48 await this.featuresRequest._promise; 48 await this.featuresRequest.promise;
49 setTimeout(this._setupFeatures.bind(this), 1); 49 setTimeout(this._setupFeatures.bind(this), 1);
50 } 50 }
51 51
diff --git a/src/stores/GlobalErrorStore.ts b/src/stores/GlobalErrorStore.ts
index c42e9a4af..be86563d0 100644
--- a/src/stores/GlobalErrorStore.ts
+++ b/src/stores/GlobalErrorStore.ts
@@ -1,4 +1,5 @@
1import { observable, action, makeObservable } from 'mobx'; 1import { observable, action, makeObservable } from 'mobx';
2import { Response } from 'electron';
2import { Actions } from '../actions/lib/actions'; 3import { Actions } from '../actions/lib/actions';
3import { ApiInterface } from '../api'; 4import { ApiInterface } from '../api';
4import { Stores } from '../@types/stores.types'; 5import { Stores } from '../@types/stores.types';
@@ -11,12 +12,8 @@ interface Message {
11 message?: string; 12 message?: string;
12 status?: number; 13 status?: number;
13 }; 14 };
14 request?: { 15 request?: Request;
15 result: any; 16 response?: Response;
16 wasExecuted: any;
17 method: any;
18 };
19 response?: any;
20 server?: any; 17 server?: any;
21 info?: any; 18 info?: any;
22 url?: string; 19 url?: string;
@@ -28,7 +25,7 @@ export default class GlobalErrorStore extends TypedStore {
28 25
29 @observable messages: Message[] = []; 26 @observable messages: Message[] = [];
30 27
31 @observable response: object = {}; 28 @observable response: Response = {} as Response;
32 29
33 // TODO: Get rid of the @ts-ignores in this function. 30 // TODO: Get rid of the @ts-ignores in this function.
34 constructor(stores: Stores, api: ApiInterface, actions: Actions) { 31 constructor(stores: Stores, api: ApiInterface, actions: Actions) {
@@ -85,21 +82,15 @@ export default class GlobalErrorStore extends TypedStore {
85 } 82 }
86 } 83 }
87 84
88 @action _handleRequests = async (request: { 85 @action _handleRequests = async (request: Request): Promise<void> => {
89 isError: any;
90 error: { json: () => object | PromiseLike<object> };
91 result: any;
92 wasExecuted: any;
93 _method: any;
94 }): Promise<void> => {
95 if (request.isError) { 86 if (request.isError) {
96 this.error = request.error; 87 this.error = request.error;
97 88
98 if (request.error.json) { 89 if (request.error && request.error.json) {
99 try { 90 try {
100 this.response = await request.error.json(); 91 this.response = await request.error.json();
101 } catch { 92 } catch {
102 this.response = {}; 93 this.response = {} as Response;
103 } 94 }
104 if (this.error?.status === 401) { 95 if (this.error?.status === 401) {
105 window['ferdium'].stores.app.authRequestFailed = true; 96 window['ferdium'].stores.app.authRequestFailed = true;
@@ -111,8 +102,8 @@ export default class GlobalErrorStore extends TypedStore {
111 request: { 102 request: {
112 result: request.result, 103 result: request.result,
113 wasExecuted: request.wasExecuted, 104 wasExecuted: request.wasExecuted,
114 method: request._method, 105 method: request.method,
115 }, 106 } as Request,
116 error: this.error, 107 error: this.error,
117 response: this.response, 108 response: this.response,
118 server: window['ferdium'].stores.settings.app.server, 109 server: window['ferdium'].stores.settings.app.server,
diff --git a/src/stores/RecipesStore.ts b/src/stores/RecipesStore.ts
index 25304e97c..07f1343f8 100644
--- a/src/stores/RecipesStore.ts
+++ b/src/stores/RecipesStore.ts
@@ -74,8 +74,8 @@ export default class RecipesStore extends TypedStore {
74 74
75 // Actions 75 // Actions
76 async _install({ recipeId }): Promise<Recipe> { 76 async _install({ recipeId }): Promise<Recipe> {
77 const recipe = await this.installRecipeRequest.execute(recipeId)._promise; 77 const recipe = await this.installRecipeRequest.execute(recipeId).promise;
78 await this.allRecipesRequest.invalidate({ immediately: true })._promise; 78 await this.allRecipesRequest.invalidate({ immediately: true }).promise;
79 79
80 return recipe; 80 return recipe;
81 } 81 }
@@ -128,7 +128,7 @@ export default class RecipesStore extends TypedStore {
128 const update = updates[i]; 128 const update = updates[i];
129 129
130 this.actions.recipe.install({ recipeId: update }); 130 this.actions.recipe.install({ recipeId: update });
131 await this.installRecipeRequest._promise; 131 await this.installRecipeRequest.promise;
132 132
133 this.installRecipeRequest.reset(); 133 this.installRecipeRequest.reset();
134 134
@@ -158,10 +158,10 @@ export default class RecipesStore extends TypedStore {
158 debug(`Recipe ${recipeId} is not installed, trying to install it`); 158 debug(`Recipe ${recipeId} is not installed, trying to install it`);
159 159
160 const recipe = await this.installRecipeRequest.execute(recipeId) 160 const recipe = await this.installRecipeRequest.execute(recipeId)
161 ._promise; 161 .promise;
162 if (recipe) { 162 if (recipe) {
163 await this.allRecipesRequest.invalidate({ immediately: true }) 163 await this.allRecipesRequest.invalidate({ immediately: true })
164 ._promise; 164 .promise;
165 router.push(`/settings/services/add/${recipeId}`); 165 router.push(`/settings/services/add/${recipeId}`);
166 } else { 166 } else {
167 router.push('/settings/recipes'); 167 router.push('/settings/recipes');
diff --git a/src/stores/RequestStore.ts b/src/stores/RequestStore.ts
index 279615e50..807f2d126 100644
--- a/src/stores/RequestStore.ts
+++ b/src/stores/RequestStore.ts
@@ -37,6 +37,9 @@ export default class RequestStore extends TypedStore {
37 ); 37 );
38 38
39 this.registerReactions([this._autoRetry.bind(this)]); 39 this.registerReactions([this._autoRetry.bind(this)]);
40
41 this.userInfoRequest = {} as CachedRequest;
42 this.servicesRequest = {} as CachedRequest;
40 } 43 }
41 44
42 async setup(): Promise<void> { 45 async setup(): Promise<void> {
diff --git a/src/stores/ServicesStore.ts b/src/stores/ServicesStore.ts
index 74810b81f..4fdd9d5ad 100644
--- a/src/stores/ServicesStore.ts
+++ b/src/stores/ServicesStore.ts
@@ -477,7 +477,7 @@ export default class ServicesStore extends TypedStore {
477 : this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData); 477 : this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData);
478 478
479 const response = await this.createServiceRequest.execute(recipeId, data) 479 const response = await this.createServiceRequest.execute(recipeId, data)
480 ._promise; 480 .promise;
481 481
482 this.allServicesRequest.patch(result => { 482 this.allServicesRequest.patch(result => {
483 if (!result) return; 483 if (!result) return;
@@ -536,7 +536,7 @@ export default class ServicesStore extends TypedStore {
536 536
537 const newData = serviceData; 537 const newData = serviceData;
538 if (serviceData.iconFile) { 538 if (serviceData.iconFile) {
539 await request._promise; 539 await request.promise;
540 540
541 newData.iconUrl = request.result.data.iconUrl; 541 newData.iconUrl = request.result.data.iconUrl;
542 newData.hasCustomUploadedIcon = true; 542 newData.hasCustomUploadedIcon = true;
@@ -562,7 +562,7 @@ export default class ServicesStore extends TypedStore {
562 ); 562 );
563 }); 563 });
564 564
565 await request._promise; 565 await request.promise;
566 this.actionStatus = request.result.status; 566 this.actionStatus = request.result.status;
567 567
568 if (service.isEnabled) { 568 if (service.isEnabled) {
@@ -596,7 +596,7 @@ export default class ServicesStore extends TypedStore {
596 remove(result, (c: Service) => c.id === serviceId); 596 remove(result, (c: Service) => c.id === serviceId);
597 }); 597 });
598 598
599 await request._promise; 599 await request.promise;
600 this.actionStatus = request.result.status; 600 this.actionStatus = request.result.status;
601 } 601 }
602 602
@@ -637,7 +637,7 @@ export default class ServicesStore extends TypedStore {
637 @action async _clearCache({ serviceId }) { 637 @action async _clearCache({ serviceId }) {
638 this.clearCacheRequest.reset(); 638 this.clearCacheRequest.reset();
639 const request = this.clearCacheRequest.execute(serviceId); 639 const request = this.clearCacheRequest.execute(serviceId);
640 await request._promise; 640 await request.promise;
641 } 641 }
642 642
643 @action _setIsActive(service: Service, state: boolean): void { 643 @action _setIsActive(service: Service, state: boolean): void {
diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts
index c5e67c966..6c8f8f20b 100644
--- a/src/stores/UserStore.ts
+++ b/src/stores/UserStore.ts
@@ -187,7 +187,7 @@ export default class UserStore extends TypedStore {
187 187
188 // Actions 188 // Actions
189 @action async _login({ email, password }): Promise<void> { 189 @action async _login({ email, password }): Promise<void> {
190 const authToken = await this.loginRequest.execute(email, password)._promise; 190 const authToken = await this.loginRequest.execute(email, password).promise;
191 this._setUserData(authToken); 191 this._setUserData(authToken);
192 192
193 this.stores.router.push('/'); 193 this.stores.router.push('/');
@@ -209,6 +209,8 @@ export default class UserStore extends TypedStore {
209 plan, 209 plan,
210 currency, 210 currency,
211 }): Promise<void> { 211 }): Promise<void> {
212 // TODO - [TS DEBT] Need to find a way proper to implement promise's then and catch in request class
213 // @ts-ignore
212 const authToken = await this.signupRequest.execute({ 214 const authToken = await this.signupRequest.execute({
213 firstname, 215 firstname,
214 lastname, 216 lastname,
@@ -231,14 +233,14 @@ export default class UserStore extends TypedStore {
231 @action async _retrievePassword({ email }): Promise<void> { 233 @action async _retrievePassword({ email }): Promise<void> {
232 const request = this.passwordRequest.execute(email); 234 const request = this.passwordRequest.execute(email);
233 235
234 await request._promise; 236 await request.promise;
235 this.actionStatus = request.result.status || []; 237 this.actionStatus = request.result.status || [];
236 } 238 }
237 239
238 @action async _invite({ invites }): Promise<void> { 240 @action async _invite({ invites }): Promise<void> {
239 const data = invites.filter(invite => invite.email !== ''); 241 const data = invites.filter(invite => invite.email !== '');
240 242
241 const response = await this.inviteRequest.execute(data)._promise; 243 const response = await this.inviteRequest.execute(data).promise;
242 244
243 this.actionStatus = response.status || []; 245 this.actionStatus = response.status || [];
244 246
@@ -251,8 +253,7 @@ export default class UserStore extends TypedStore {
251 @action async _update({ userData }): Promise<void> { 253 @action async _update({ userData }): Promise<void> {
252 if (!this.isLoggedIn) return; 254 if (!this.isLoggedIn) return;
253 255
254 const response = await this.updateUserInfoRequest.execute(userData) 256 const response = await this.updateUserInfoRequest.execute(userData).promise;
255 ._promise;
256 257
257 this.getUserInfoRequest.patch(() => response.data); 258 this.getUserInfoRequest.patch(() => response.data);
258 this.actionStatus = response.status || []; 259 this.actionStatus = response.status || [];
@@ -299,7 +300,7 @@ export default class UserStore extends TypedStore {
299 data: service, 300 data: service,
300 }); 301 });
301 // eslint-disable-next-line no-await-in-loop 302 // eslint-disable-next-line no-await-in-loop
302 await this.stores.services.createServiceRequest._promise; 303 await this.stores.services.createServiceRequest.promise;
303 } 304 }
304 305
305 this.isImportLegacyServicesExecuting = false; 306 this.isImportLegacyServicesExecuting = false;
@@ -349,7 +350,7 @@ export default class UserStore extends TypedStore {
349 if (this.isLoggedIn) { 350 if (this.isLoggedIn) {
350 let data; 351 let data;
351 try { 352 try {
352 data = await this.getUserInfoRequest.execute()._promise; 353 data = await this.getUserInfoRequest.execute().promise;
353 } catch { 354 } catch {
354 return; 355 return;
355 } 356 }
@@ -406,7 +407,7 @@ export default class UserStore extends TypedStore {
406 407
407 async _migrateUserLocale(): Promise<void> { 408 async _migrateUserLocale(): Promise<void> {
408 try { 409 try {
409 await this.getUserInfoRequest._promise; 410 await this.getUserInfoRequest.promise;
410 } catch { 411 } catch {
411 return; 412 return;
412 } 413 }
diff --git a/src/stores/lib/CachedRequest.js b/src/stores/lib/CachedRequest.ts
index a6dd47f7d..25cc365e2 100644
--- a/src/stores/lib/CachedRequest.js
+++ b/src/stores/lib/CachedRequest.ts
@@ -3,29 +3,33 @@ import { isEqual, remove } from 'lodash';
3import Request from './Request'; 3import Request from './Request';
4 4
5export default class CachedRequest extends Request { 5export default class CachedRequest extends Request {
6 _apiCalls = []; 6 _apiCalls: any[] = [];
7 7
8 _isInvalidated = true; 8 _isInvalidated = true;
9 9
10 execute(...callArgs) { 10 execute(...callArgs): this {
11 // Do not continue if this request is already loading 11 // Do not continue if this request is already loading
12 if (this._isWaitingForResponse) return this; 12 if (this.isWaitingForResponse) {
13 return this;
14 }
13 15
14 // Very simple caching strategy -> only continue if the call / args changed 16 // Very simple caching strategy -> only continue if the call / args changed
15 // or the request was invalidated manually from outside 17 // or the request was invalidated manually from outside
16 const existingApiCall = this._findApiCall(callArgs); 18 const existingApiCall = this._findApiCall(callArgs);
17 19
18 // Invalidate if new or different api call will be done 20 // Invalidate if new or different api call will be done
19 if (existingApiCall && existingApiCall !== this._currentApiCall) { 21 if (existingApiCall && existingApiCall !== this.currentApiCall) {
20 this._isInvalidated = true; 22 this._isInvalidated = true;
21 this._currentApiCall = existingApiCall; 23 this.currentApiCall = existingApiCall;
22 } else if (!existingApiCall) { 24 } else if (!existingApiCall) {
23 this._isInvalidated = true; 25 this._isInvalidated = true;
24 this._currentApiCall = this._addApiCall(callArgs); 26 this.currentApiCall = this._addApiCall(callArgs);
25 } 27 }
26 28
27 // Do not continue if this request is not invalidated (see above) 29 // Do not continue if this request is not invalidated (see above)
28 if (!this._isInvalidated) return this; 30 if (!this._isInvalidated) {
31 return this;
32 }
29 33
30 // This timeout is necessary to avoid warnings from mobx 34 // This timeout is necessary to avoid warnings from mobx
31 // regarding triggering actions as side-effect of getters 35 // regarding triggering actions as side-effect of getters
@@ -41,18 +45,18 @@ export default class CachedRequest extends Request {
41 ); 45 );
42 46
43 // Issue api call & save it as promise that is handled to update the results of the operation 47 // Issue api call & save it as promise that is handled to update the results of the operation
44 this._promise = new Promise(resolve => { 48 this.promise = new Promise(resolve => {
45 this._api[this._method](...callArgs) 49 this.api[this.method](...callArgs)
46 .then(result => { 50 .then(result => {
47 setTimeout( 51 setTimeout(
48 action(() => { 52 action(() => {
49 this.result = result; 53 this.result = result;
50 if (this._currentApiCall) this._currentApiCall.result = result; 54 if (this.currentApiCall) this.currentApiCall.result = result;
51 this.isExecuting = false; 55 this.isExecuting = false;
52 this.isError = false; 56 this.isError = false;
53 this.wasExecuted = true; 57 this.wasExecuted = true;
54 this._isInvalidated = false; 58 this._isInvalidated = false;
55 this._isWaitingForResponse = false; 59 this.isWaitingForResponse = false;
56 this._triggerHooks(); 60 this._triggerHooks();
57 resolve(result); 61 resolve(result);
58 }), 62 }),
@@ -68,7 +72,7 @@ export default class CachedRequest extends Request {
68 this.isExecuting = false; 72 this.isExecuting = false;
69 this.isError = true; 73 this.isError = true;
70 this.wasExecuted = true; 74 this.wasExecuted = true;
71 this._isWaitingForResponse = false; 75 this.isWaitingForResponse = false;
72 this._triggerHooks(); 76 this._triggerHooks();
73 // reject(error); 77 // reject(error);
74 }), 78 }),
@@ -78,26 +82,27 @@ export default class CachedRequest extends Request {
78 ); 82 );
79 }); 83 });
80 84
81 this._isWaitingForResponse = true; 85 this.isWaitingForResponse = true;
82 return this; 86 return this;
83 } 87 }
84 88
85 // eslint-disable-next-line unicorn/no-object-as-default-parameter 89 static defaultOptions = { immediately: false };
86 invalidate(options = { immediately: false }) { 90
91 invalidate(options = CachedRequest.defaultOptions): this {
87 this._isInvalidated = true; 92 this._isInvalidated = true;
88 if (options.immediately && this._currentApiCall) { 93 if (options.immediately && this.currentApiCall) {
89 return this.execute(...this._currentApiCall.args); 94 return this.execute(...this.currentApiCall.args);
90 } 95 }
91 return this; 96 return this;
92 } 97 }
93 98
94 patch(modify) { 99 patch(modify): Promise<this> {
95 return new Promise(resolve => { 100 return new Promise(resolve => {
96 setTimeout( 101 setTimeout(
97 action(() => { 102 action(() => {
98 const override = modify(this.result); 103 const override = modify(this.result);
99 if (override !== undefined) this.result = override; 104 if (override !== undefined) this.result = override;
100 if (this._currentApiCall) this._currentApiCall.result = this.result; 105 if (this.currentApiCall) this.currentApiCall.result = this.result;
101 resolve(this); 106 resolve(this);
102 }), 107 }),
103 0, 108 0,
@@ -105,17 +110,17 @@ export default class CachedRequest extends Request {
105 }); 110 });
106 } 111 }
107 112
108 removeCacheForCallWith(...args) { 113 removeCacheForCallWith(...args: any): void {
109 remove(this._apiCalls, c => isEqual(c.args, args)); 114 remove(this._apiCalls, c => isEqual(c.args, args));
110 } 115 }
111 116
112 _addApiCall(args) { 117 _addApiCall(args: any) {
113 const newCall = { args, result: null }; 118 const newCall = { args, result: null };
114 this._apiCalls.push(newCall); 119 this._apiCalls.push(newCall);
115 return newCall; 120 return newCall;
116 } 121 }
117 122
118 _findApiCall(args) { 123 _findApiCall(args: any) {
119 return this._apiCalls.find(c => isEqual(c.args, args)); 124 return this._apiCalls.find(c => isEqual(c.args, args));
120 } 125 }
121} 126}
diff --git a/src/stores/lib/Request.js b/src/stores/lib/Request.ts
index 60c943a42..f9424ac99 100644
--- a/src/stores/lib/Request.js
+++ b/src/stores/lib/Request.ts
@@ -1,16 +1,18 @@
1import { observable, action, computed, makeObservable } from 'mobx'; 1import { observable, action, computed, makeObservable } from 'mobx';
2import { isEqual } from 'lodash/fp'; 2import { isEqual } from 'lodash/fp';
3 3
4type Hook = (request: Request) => void;
5
4export default class Request { 6export default class Request {
5 static _hooks = []; 7 static _hooks: Hook[] = [];
6 8
7 static registerHook(hook) { 9 static registerHook(hook: Hook) {
8 Request._hooks.push(hook); 10 Request._hooks.push(hook);
9 } 11 }
10 12
11 @observable result = null; 13 @observable result: any = null;
12 14
13 @observable error = null; 15 @observable error: any = null;
14 16
15 @observable isExecuting = false; 17 @observable isExecuting = false;
16 18
@@ -18,43 +20,47 @@ export default class Request {
18 20
19 @observable wasExecuted = false; 21 @observable wasExecuted = false;
20 22
21 @action _reset() { 23 promise: any = Promise;
22 this.error = null;
23 this.result = null;
24 this.isExecuting = false;
25 this.isError = false;
26 this.wasExecuted = false;
27 this._isWaitingForResponse = false;
28 this._promise = Promise;
29 24
30 return this; 25 protected api: any = {};
31 }
32 26
33 _promise = Promise; 27 method = '';
34 28
35 _api = {}; 29 protected isWaitingForResponse = false;
36 30
37 _method = ''; 31 protected currentApiCall: any = null;
38 32
39 _isWaitingForResponse = false; 33 retry = () => this.reload();
40 34
41 _currentApiCall = null; 35 reset = () => this._reset();
42 36
43 constructor(api, method) { 37 constructor(api, method) {
44 makeObservable(this); 38 makeObservable(this);
45 39
46 this._api = api; 40 this.api = api;
47 this._method = method; 41 this.method = method;
42 }
43
44 @action _reset(): this {
45 this.error = null;
46 this.result = null;
47 this.isExecuting = false;
48 this.isError = false;
49 this.wasExecuted = false;
50 this.isWaitingForResponse = false;
51 this.promise = Promise;
52
53 return this;
48 } 54 }
49 55
50 execute(...callArgs) { 56 execute(...callArgs: any[]): this {
51 // Do not continue if this request is already loading 57 // Do not continue if this request is already loading
52 if (this._isWaitingForResponse) return this; 58 if (this.isWaitingForResponse) return this;
53 59
54 if (!this._api[this._method]) { 60 if (!this.api[this.method]) {
55 throw new Error( 61 throw new Error(
56 `Missing method <${this._method}> on api object:`, 62 `Missing method <${this.method}> on api object:`,
57 this._api, 63 this.api,
58 ); 64 );
59 } 65 }
60 66
@@ -68,18 +74,18 @@ export default class Request {
68 ); 74 );
69 75
70 // Issue api call & save it as promise that is handled to update the results of the operation 76 // Issue api call & save it as promise that is handled to update the results of the operation
71 this._promise = new Promise((resolve, reject) => { 77 this.promise = new Promise((resolve, reject) => {
72 this._api[this._method](...callArgs) 78 this.api[this.method](...callArgs)
73 .then(result => { 79 .then(result => {
74 setTimeout( 80 setTimeout(
75 action(() => { 81 action(() => {
76 this.error = null; 82 this.error = null;
77 this.result = result; 83 this.result = result;
78 if (this._currentApiCall) this._currentApiCall.result = result; 84 if (this.currentApiCall) this.currentApiCall.result = result;
79 this.isExecuting = false; 85 this.isExecuting = false;
80 this.isError = false; 86 this.isError = false;
81 this.wasExecuted = true; 87 this.wasExecuted = true;
82 this._isWaitingForResponse = false; 88 this.isWaitingForResponse = false;
83 this._triggerHooks(); 89 this._triggerHooks();
84 resolve(result); 90 resolve(result);
85 }), 91 }),
@@ -95,7 +101,7 @@ export default class Request {
95 this.isExecuting = false; 101 this.isExecuting = false;
96 this.isError = true; 102 this.isError = true;
97 this.wasExecuted = true; 103 this.wasExecuted = true;
98 this._isWaitingForResponse = false; 104 this.isWaitingForResponse = false;
99 this._triggerHooks(); 105 this._triggerHooks();
100 reject(error); 106 reject(error);
101 }), 107 }),
@@ -105,51 +111,49 @@ export default class Request {
105 ); 111 );
106 }); 112 });
107 113
108 this._isWaitingForResponse = true; 114 this.isWaitingForResponse = true;
109 this._currentApiCall = { args: callArgs, result: null }; 115 this.currentApiCall = { args: callArgs, result: null };
110 return this; 116 return this;
111 } 117 }
112 118
113 reload() { 119 reload(): this {
114 const args = this._currentApiCall ? this._currentApiCall.args : []; 120 const args = this.currentApiCall ? this.currentApiCall.args : [];
115 this.error = null; 121 this.error = null;
116 return this.execute(...args); 122 return this.execute(...args);
117 } 123 }
118 124
119 retry = () => this.reload(); 125 isExecutingWithArgs(...args: any[]): boolean {
120
121 isExecutingWithArgs(...args) {
122 return ( 126 return (
123 this.isExecuting && 127 this.isExecuting &&
124 this._currentApiCall && 128 this.currentApiCall &&
125 isEqual(this._currentApiCall.args, args) 129 isEqual(this.currentApiCall.args, args)
126 ); 130 );
127 } 131 }
128 132
129 @computed get isExecutingFirstTime() { 133 @computed get isExecutingFirstTime(): boolean {
130 return !this.wasExecuted && this.isExecuting; 134 return !this.wasExecuted && this.isExecuting;
131 } 135 }
132 136
133 /* eslint-disable unicorn/no-thenable */ 137 /* eslint-disable unicorn/no-thenable */
134 then(...args) { 138 then(...args: any[]) {
135 if (!this._promise) 139 if (!this.promise)
136 throw new Error( 140 throw new Error(
137 'You have to call Request::execute before you can access it as promise', 141 'You have to call Request::execute before you can access it as promise',
138 ); 142 );
139 return this._promise.then(...args); 143 return this.promise.then(...args);
140 } 144 }
141 145
142 catch(...args) { 146 catch(...args: any[]) {
143 if (!this._promise) 147 if (!this.promise)
144 throw new Error( 148 throw new Error(
145 'You have to call Request::execute before you can access it as promise', 149 'You have to call Request::execute before you can access it as promise',
146 ); 150 );
147 return this._promise.catch(...args); 151 return this.promise.catch(...args);
148 } 152 }
149 153
150 _triggerHooks() { 154 _triggerHooks(): void {
151 for (const hook of Request._hooks) hook(this); 155 for (const hook of Request._hooks) {
156 hook(this);
157 }
152 } 158 }
153
154 reset = () => this._reset();
155} 159}