From 51d514e950f090275cedfdca017e715859624fd6 Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Fri, 18 Mar 2022 11:25:30 +0100
Subject: [PATCH] Move logic to AppController

---
 openbis_ng_ui/src/js/components/App.jsx       |  60 +--
 .../src/js/components/AppController.js        | 409 +++++++++++++++++-
 .../js/components/common/ComponentContext.js  |   5 -
 .../js/components/common/browser/Browser.jsx  |  23 +-
 .../common/browser/BrowserController.js       |   6 +-
 .../js/components/common/content/Content.jsx  |  41 +-
 .../components/common/form/LinkToObject.jsx   |  19 +-
 .../src/js/components/common/menu/Menu.jsx    |  58 ++-
 .../common/page/PageControllerChanged.js      |   9 +-
 .../common/page/PageControllerLoad.js         |   4 +-
 .../common/page/PageControllerSave.js         |  28 +-
 .../src/js/components/login/Login.jsx         |  20 +-
 .../tools/browser/ToolBrowserController.js    |  10 +-
 .../components/tools/common/HistoryGrid.jsx   |   5 +-
 .../plugin/PluginFormControllerEvaluate.js    |   4 +-
 .../form/query/QueryFormControllerExecute.js  |   4 +-
 .../js/components/tools/search/ToolSearch.jsx |   5 +-
 .../types/browser/TypeBrowserController.js    |  10 +-
 .../EntityTypeFormPreviewProperty.jsx         |   8 +-
 .../js/components/types/search/TypeSearch.jsx |   5 +-
 .../users/browser/UserBrowserController.js    |  18 +-
 .../js/components/users/search/UserSearch.jsx |   5 +-
 openbis_ng_ui/src/js/store/history.js         |   4 +-
 23 files changed, 518 insertions(+), 242 deletions(-)

diff --git a/openbis_ng_ui/src/js/components/App.jsx b/openbis_ng_ui/src/js/components/App.jsx
index e06b48c7d88..4e3396549f2 100644
--- a/openbis_ng_ui/src/js/components/App.jsx
+++ b/openbis_ng_ui/src/js/components/App.jsx
@@ -1,12 +1,10 @@
 import React from 'react'
 import _ from 'lodash'
-import { connect } from 'react-redux'
+import autoBind from 'auto-bind'
 import { withStyles } from '@material-ui/core/styles'
 import logger from '@src/js/common/logger.js'
 import util from '@src/js/common/util.js'
 import pages from '@src/js/common/consts/pages.js'
-import actions from '@src/js/store/actions/actions.js'
-import selectors from '@src/js/store/selectors/selectors.js'
 
 import Loading from '@src/js/components/common/loading/Loading.jsx'
 import Error from '@src/js/components/common/error/Error.jsx'
@@ -45,30 +43,10 @@ const pageToComponent = {
   [pages.TOOLS]: Tools
 }
 
-function mapStateToProps(state) {
-  return {
-    initialized: selectors.getInitialized(state),
-    loading: selectors.getLoading(state),
-    session: selectors.getSession(state),
-    currentPage: selectors.getCurrentPage(state),
-    error: selectors.getError(state)
-  }
-}
-
-function mapDispatchToProps(dispatch) {
-  return {
-    init: () => {
-      dispatch(actions.init())
-    },
-    errorClosed: () => {
-      dispatch(actions.errorChange(null))
-    }
-  }
-}
-
 class App extends React.Component {
   constructor(props) {
     super(props)
+    autoBind(this)
 
     this.state = {}
 
@@ -82,34 +60,39 @@ class App extends React.Component {
   }
 
   componentDidMount() {
-    this.props.init()
+    this.controller.load()
+  }
+
+  handleErrorClosed() {
+    AppController.errorChange(null)
   }
 
   render() {
     logger.log(logger.DEBUG, 'App.render')
 
     return (
-      <AppController.LastObjectModificationsContext.Provider
-        value={AppController.getLastObjectModifications()}
-      >
-        <Loading loading={this.props.loading}>
-          <Error error={this.props.error} errorClosed={this.props.errorClosed}>
-            {this.props.initialized && this.renderPage()}
+      <AppController.AppContext.Provider value={this.state}>
+        <Loading loading={AppController.getLoading()}>
+          <Error
+            error={AppController.getError()}
+            errorClosed={this.handleErrorClosed}
+          >
+            {AppController.getLoaded() && this.renderPage()}
           </Error>
         </Loading>
-      </AppController.LastObjectModificationsContext.Provider>
+      </AppController.AppContext.Provider>
     )
   }
 
   renderPage() {
     const classes = this.props.classes
 
-    if (this.props.session) {
+    if (AppController.getSession()) {
       return (
         <div className={classes.container}>
-          <Menu page={this.props.currentPage} />
+          <Menu />
           {_.map(pageToComponent, (PageComponent, page) => {
-            let visible = this.props.currentPage === page
+            let visible = AppController.getCurrentPage() === page
             return (
               <div
                 key={page}
@@ -125,12 +108,9 @@ class App extends React.Component {
         </div>
       )
     } else {
-      return <Login disabled={this.props.loading} />
+      return <Login disabled={AppController.getLoading()} />
     }
   }
 }
 
-export default _.flow(
-  connect(mapStateToProps, mapDispatchToProps),
-  withStyles(styles)
-)(App)
+export default _.flow(withStyles(styles), AppController.withState())(App)
diff --git a/openbis_ng_ui/src/js/components/AppController.js b/openbis_ng_ui/src/js/components/AppController.js
index 34e2ef349b0..a686d679798 100644
--- a/openbis_ng_ui/src/js/components/AppController.js
+++ b/openbis_ng_ui/src/js/components/AppController.js
@@ -1,25 +1,404 @@
+import _ from 'lodash'
 import React from 'react'
+import openbis from '@src/js/services/openbis.js'
+import objectType from '@src/js/common/consts/objectType.js'
+import objectOperation from '@src/js/common/consts/objectOperation.js'
+import routes from '@src/js/common/consts/routes.js'
+import cookie from '@src/js/common/cookie.js'
+import history from '@src/js/store/history.js'
 
 export class AppController {
   constructor() {
-    this.LastObjectModificationsContext = React.createContext()
+    this.AppContext = React.createContext()
   }
 
   init(context) {
     this.context = context
+    context.initState(this.initialState())
+  }
 
-    const initialState = {
+  initialState() {
+    return {
+      loaded: false,
+      loading: false,
+      session: null,
+      route: null,
+      search: null,
+      pages: [],
+      error: null,
       lastObjectModifications: {}
     }
+  }
+
+  async load() {
+    const { loaded } = this.context.getState()
+
+    if (!loaded) {
+      try {
+        await this.context.setState({ loading: true })
+        await openbis.init()
+
+        const sessionToken = cookie.read(cookie.OPENBIS_COOKIE)
+
+        if (sessionToken) {
+          try {
+            openbis.useSession(sessionToken)
+
+            const sessionInformation = await openbis.getSessionInformation()
+
+            if (sessionInformation) {
+              await this.context.setState({
+                session: {
+                  sessionToken: sessionToken,
+                  userName: sessionInformation.userName
+                }
+              })
+            } else {
+              openbis.useSession(null)
+            }
+          } catch (e) {
+            openbis.useSession(null)
+          }
+        }
+
+        await this.context.setState({ loaded: true })
+      } catch (e) {
+        await this.context.setState({ error: e })
+      } finally {
+        await this.context.setState({ loading: false })
+      }
+    }
+  }
+
+  async login(username, password) {
+    try {
+      await this.context.setState({ loading: true })
+
+      const sessionToken = await openbis.login(username, password)
+
+      this.context.setState({
+        session: {
+          sessionToken: sessionToken,
+          userName: username
+        }
+      })
+      cookie.create(cookie.OPENBIS_COOKIE, sessionToken, 7)
+
+      const { route } = this.context.getState()
+      const routeObject = routes.parse(route)
+      await this.context.setState({ route: routeObject.path })
+    } catch (e) {
+      await this.context.setState({ error: 'Incorrect user or password' })
+    } finally {
+      await this.context.setState({ loading: false })
+    }
+  }
+
+  async logout() {
+    try {
+      await this.context.setState({ loading: true })
+      await openbis.logout()
+      this.context.setState(state => ({
+        ...this.initialState(),
+        loaded: state.loaded
+      }))
+      cookie.erase(cookie.OPENBIS_COOKIE)
+      await this.context.setState({ route: '/' })
+    } catch (e) {
+      await this.context.setState({ error: e })
+    } finally {
+      await this.context.setState({ loading: false })
+    }
+  }
+
+  async search(page, text) {
+    if (text.trim().length > 0) {
+      await this.objectOpen(page, objectType.SEARCH, text.trim())
+    }
+    await this.context.setState({ search: '' })
+  }
+
+  async searchChange(text) {
+    await this.context.setState({ search: text })
+  }
+
+  async pageChange(page) {
+    const pageRoute = this.getCurrentRoute(page)
+    if (pageRoute) {
+      await this.context.setState({ route: pageRoute })
+    } else {
+      const route = routes.format({ page })
+      await this.context.setState({ route: route })
+    }
+  }
+
+  async errorChange(error) {
+    await this.context.setState({ error: error })
+  }
+
+  async routeChange(path) {
+    const route = routes.parse(path)
+
+    if (route.type && route.id) {
+      const object = { type: route.type, id: route.id }
+      const openTabs = this.getOpenTabs(route.page)
+
+      if (openTabs) {
+        let found = false
+        let id = 1
+
+        openTabs.forEach(openTab => {
+          if (_.isMatch(openTab.object, object)) {
+            found = true
+          }
+          if (openTab.id >= id) {
+            id = openTab.id + 1
+          }
+        })
+
+        if (!found) {
+          await this.addOpenTab(route.page, id, { id, object })
+        }
+      }
+    }
+
+    await this.context.setState({ route: path })
+    await this.setCurrentRoute(route.page, path)
+  }
+
+  async routeReplace(route, state) {
+    history.replace(route, state)
+  }
+
+  async objectNew(page, type) {
+    let id = 1
+    const openObjects = this.getOpenObjects(page)
+    openObjects.forEach(openObject => {
+      if (openObject.type === type) {
+        id++
+      }
+    })
+
+    const route = routes.format({ page, type, id })
+    await this.context.setState({ route: route })
+  }
+
+  async objectCreate(page, oldType, oldId, newType, newId) {
+    const openTabs = this.getOpenTabs(page)
+    const oldTab = _.find(openTabs, { object: { type: oldType, id: oldId } })
+
+    if (oldTab) {
+      const newTab = {
+        ...oldTab,
+        object: { type: newType, id: newId },
+        changed: false
+      }
+      await this.replaceOpenTab(page, oldTab.id, newTab)
+      await this.setLastObjectModification(
+        newType,
+        objectOperation.CREATE,
+        Date.now()
+      )
+
+      const route = routes.format({ page, type: newType, id: newId })
+      await this.routeReplace(route)
+    }
+  }
+
+  async objectOpen(page, type, id) {
+    const route = routes.format({ page, type, id })
+    await this.routeChange(route)
+  }
+
+  async objectUpdate(type) {
+    await this.setLastObjectModification(
+      type,
+      objectOperation.UPDATE,
+      Date.now()
+    )
+  }
+
+  async objectDelete(page, type, id) {
+    await this.objectClose(page, type, id)
+    await this.setLastObjectModification(
+      type,
+      objectOperation.DELETE,
+      Date.now()
+    )
+  }
+
+  async objectChange(page, type, id, changed) {
+    const openTabs = this.getOpenTabs(page)
+    const oldTab = _.find(openTabs, { object: { type, id } })
+
+    if (oldTab) {
+      const newTab = { ...oldTab, changed }
+      await this.replaceOpenTab(page, oldTab.id, newTab)
+    }
+  }
+
+  async objectClose(page, type, id) {
+    const openTabs = this.getOpenTabs(page)
+    const objectToClose = { type, id }
+
+    let selectedObject = this.getSelectedObject(page)
+    if (selectedObject && _.isEqual(selectedObject, objectToClose)) {
+      if (_.size(openTabs) === 1) {
+        selectedObject = null
+      } else {
+        let selectedIndex = _.findIndex(openTabs, { object: selectedObject })
+        if (selectedIndex === 0) {
+          selectedObject = openTabs[selectedIndex + 1].object
+        } else {
+          selectedObject = openTabs[selectedIndex - 1].object
+        }
+      }
+    }
+
+    let tabToClose = _.find(openTabs, { object: objectToClose })
+    if (tabToClose) {
+      await this.removeOpenTab(page, tabToClose.id)
+    }
+
+    if (selectedObject) {
+      const route = routes.format({
+        page,
+        type: selectedObject.type,
+        id: selectedObject.id
+      })
+      await this.routeChange(route)
+    } else {
+      const route = routes.format({ page })
+      await this.routeChange(route)
+    }
+  }
+
+  getLoaded() {
+    return this.context.getState().loaded
+  }
+
+  getLoading() {
+    return this.context.getState().loading
+  }
+
+  getSession() {
+    return this.context.getState().session
+  }
+
+  getRoute() {
+    return this.context.getState().route
+  }
+
+  getSearch() {
+    return this.context.getState().search
+  }
+
+  getError() {
+    return this.context.getState().error
+  }
+
+  getCurrentPage() {
+    const { route } = this.context.getState()
+    return routes.parse(route).page
+  }
 
-    context.initState(initialState)
+  getCurrentRoute(page) {
+    const { pages } = this.context.getState()
+    return pages[page] ? pages[page].currentRoute : null
+  }
+
+  async setCurrentRoute(page, route) {
+    await this.context.setState(state => ({
+      pages: {
+        ...state.pages,
+        [page]: {
+          ...state.pages[page],
+          currentRoute: route
+        }
+      }
+    }))
+  }
+
+  getSelectedObject(page) {
+    const path = this.getCurrentRoute(page)
+    if (path) {
+      const route = routes.parse(path)
+      if (route && route.type && route.id) {
+        return { type: route.type, id: route.id }
+      }
+    }
+    return null
+  }
+
+  getSelectedTab(page) {
+    const selectedObject = this.getSelectedObject(page)
+    if (selectedObject) {
+      const openTabs = this.getOpenTabs(page)
+      return _.find(openTabs, { object: selectedObject })
+    } else {
+      return null
+    }
+  }
+
+  getOpenObjects(page) {
+    const openTabs = this.getOpenTabs(page)
+    return openTabs.map(openTab => {
+      return openTab.object
+    })
+  }
+
+  getOpenTabs(page) {
+    const { pages } = this.context.getState()
+    return (pages[page] && pages[page].openTabs) || []
+  }
+
+  async setOpenTabs(page, newOpenTabs) {
+    await this.context.setState(state => ({
+      pages: {
+        ...state.pages,
+        [page]: {
+          ...state.pages[page],
+          openTabs: newOpenTabs
+        }
+      }
+    }))
+  }
+
+  async addOpenTab(page, id, tab) {
+    const openTabs = this.getOpenTabs(page)
+    const index = _.findIndex(openTabs, { id: id }, _.isMatch)
+    if (index === -1) {
+      const newOpenTabs = Array.from(openTabs)
+      newOpenTabs.push(tab)
+      await this.setOpenTabs(page, newOpenTabs)
+    }
+  }
+
+  async removeOpenTab(page, id) {
+    const openTabs = this.getOpenTabs(page)
+    const index = _.findIndex(openTabs, { id: id }, _.isMatch)
+    if (index !== -1) {
+      const newOpenTabs = Array.from(openTabs)
+      newOpenTabs.splice(index, 1)
+      await this.setOpenTabs(page, newOpenTabs)
+    }
+  }
+
+  async replaceOpenTab(page, id, tab) {
+    const openTabs = this.getOpenTabs(page)
+    const index = _.findIndex(openTabs, { id: id }, _.isMatch)
+    if (index !== -1) {
+      const newOpenTabs = Array.from(openTabs)
+      newOpenTabs[index] = tab
+      await this.setOpenTabs(page, newOpenTabs)
+    }
   }
 
   getLastObjectModifications() {
     return this.context.getState().lastObjectModifications
   }
 
-  setLastObjectModification(type, operation, timestamp) {
+  async setLastObjectModification(type, operation, timestamp) {
     const { lastObjectModifications } = this.context.getState()
 
     if (
@@ -27,7 +406,7 @@ export class AppController {
       !lastObjectModifications[type][operation] ||
       lastObjectModifications[type][operation] < timestamp
     ) {
-      this.context.setState({
+      await this.context.setState({
         lastObjectModifications: {
           ...lastObjectModifications,
           [type]: { ...lastObjectModifications[type], [operation]: timestamp }
@@ -36,22 +415,18 @@ export class AppController {
     }
   }
 
-  withLastObjectModifications() {
-    return this._withContext(
-      this.LastObjectModificationsContext,
-      'lastObjectModifications'
-    )
-  }
-
-  _withContext(Context, propertyName) {
+  withState(additionalPropertiesFn) {
     const WithContext = Component => {
       const WithConsumer = props => {
-        return React.createElement(Context.Consumer, {}, value =>
-          React.createElement(Component, {
+        return React.createElement(this.AppContext.Consumer, {}, () => {
+          const additionalProperties = additionalPropertiesFn
+            ? additionalPropertiesFn(props)
+            : {}
+          return React.createElement(Component, {
             ...props,
-            [propertyName]: value
+            ...additionalProperties
           })
-        )
+        })
       }
       WithConsumer.displayName = 'WithConsumer'
       return WithConsumer
diff --git a/openbis_ng_ui/src/js/components/common/ComponentContext.js b/openbis_ng_ui/src/js/components/common/ComponentContext.js
index 35c7f64f2b3..bbcf0830ff2 100644
--- a/openbis_ng_ui/src/js/components/common/ComponentContext.js
+++ b/openbis_ng_ui/src/js/components/common/ComponentContext.js
@@ -13,7 +13,6 @@ export default class ComponentContext {
       return component.state
     }
     this.setStateFn = component.setState.bind(component)
-    this.dispatchFn = component.props.dispatch
   }
 
   initState(initialState) {
@@ -36,10 +35,6 @@ export default class ComponentContext {
     })
   }
 
-  dispatch(action) {
-    this.dispatchFn(action)
-  }
-
   getFacade() {
     return this.facade
   }
diff --git a/openbis_ng_ui/src/js/components/common/browser/Browser.jsx b/openbis_ng_ui/src/js/components/common/browser/Browser.jsx
index f1980f18cc7..e8919babdea 100644
--- a/openbis_ng_ui/src/js/components/common/browser/Browser.jsx
+++ b/openbis_ng_ui/src/js/components/common/browser/Browser.jsx
@@ -1,6 +1,5 @@
 import _ from 'lodash'
 import React from 'react'
-import { connect } from 'react-redux'
 import { Resizable } from 're-resizable'
 import { withStyles } from '@material-ui/core/styles'
 import FilterField from '@src/js/components/common/form/FilterField.jsx'
@@ -9,7 +8,6 @@ import BrowserNodes from '@src/js/components/common/browser/BrowserNodes.jsx'
 import BrowserButtons from '@src/js/components/common/browser/BrowserButtons.jsx'
 import BrowserDialogRemoveNode from '@src/js/components/common/browser/BrowserDialogRemoveNode.jsx'
 import AppController from '@src/js/components/AppController.js'
-import selectors from '@src/js/store/selectors/selectors.js'
 import logger from '@src/js/common/logger.js'
 
 const styles = theme => ({
@@ -29,18 +27,6 @@ const styles = theme => ({
   }
 })
 
-function mapStateToProps() {
-  return (state, ownProps) => {
-    return {
-      session: selectors.getSession(state),
-      selectedObject: selectors.getSelectedObject(
-        state,
-        ownProps.controller.getPage()
-      )
-    }
-  }
-}
-
 class Browser extends React.PureComponent {
   constructor(props) {
     super(props)
@@ -125,7 +111,12 @@ class Browser extends React.PureComponent {
 }
 
 export default _.flow(
-  connect(mapStateToProps),
   withStyles(styles),
-  AppController.withLastObjectModifications()
+  AppController.withState(ownProps => ({
+    session: AppController.getSession(),
+    selectedObject: AppController.getSelectedObject(
+      ownProps.controller.getPage()
+    ),
+    lastObjectModifications: AppController.getLastObjectModifications()
+  }))
 )(Browser)
diff --git a/openbis_ng_ui/src/js/components/common/browser/BrowserController.js b/openbis_ng_ui/src/js/components/common/browser/BrowserController.js
index 2565f94de6b..6dae7cd134b 100644
--- a/openbis_ng_ui/src/js/components/common/browser/BrowserController.js
+++ b/openbis_ng_ui/src/js/components/common/browser/BrowserController.js
@@ -1,6 +1,6 @@
 import _ from 'lodash'
 import autoBind from 'auto-bind'
-import actions from '@src/js/store/actions/actions.js'
+import AppController from '@src/js/components/AppController.js'
 
 export default class BrowserController {
   doGetPage() {
@@ -175,9 +175,7 @@ export default class BrowserController {
     })
 
     if (nodeObject) {
-      this.context.dispatch(
-        actions.objectOpen(this.getPage(), nodeObject.type, nodeObject.id)
-      )
+      AppController.objectOpen(this.getPage(), nodeObject.type, nodeObject.id)
     }
 
     const newNodes = this._setNodesSelected(nodes, nodeId, nodeObject)
diff --git a/openbis_ng_ui/src/js/components/common/content/Content.jsx b/openbis_ng_ui/src/js/components/common/content/Content.jsx
index c034a4ac1dd..b84766ad618 100644
--- a/openbis_ng_ui/src/js/components/common/content/Content.jsx
+++ b/openbis_ng_ui/src/js/components/common/content/Content.jsx
@@ -1,11 +1,9 @@
 import _ from 'lodash'
 import React from 'react'
-import { connect } from 'react-redux'
 import { withStyles } from '@material-ui/core/styles'
 import ErrorBoundary from '@src/js/components/common/error/ErrorBoundary.jsx'
 import ContentTabs from '@src/js/components/common/content/ContentTabs.jsx'
-import selectors from '@src/js/store/selectors/selectors.js'
-import actions from '@src/js/store/actions/actions.js'
+import AppController from '@src/js/components/AppController.js'
 import util from '@src/js/common/util.js'
 import logger from '@src/js/common/logger.js'
 
@@ -31,31 +29,15 @@ const styles = {
   }
 }
 
-function mapStateToProps() {
-  return (state, ownProps) => {
-    return {
-      openTabs: selectors.getOpenTabs(state, ownProps.page),
-      selectedTab: selectors.getSelectedTab(state, ownProps.page)
-    }
+class Content extends React.Component {
+  handleTabSelect(tab) {
+    AppController.objectOpen(this.props.page, tab.object.type, tab.object.id)
   }
-}
 
-function mapDispatchToProps(dispatch, ownProps) {
-  return {
-    tabSelect: tab => {
-      dispatch(
-        actions.objectOpen(ownProps.page, tab.object.type, tab.object.id)
-      )
-    },
-    tabClose: tab => {
-      dispatch(
-        actions.objectClose(ownProps.page, tab.object.type, tab.object.id)
-      )
-    }
+  handleTabClose(tab) {
+    AppController.objectClose(this.props.page, tab.object.type, tab.object.id)
   }
-}
 
-class Content extends React.Component {
   render() {
     logger.log(logger.DEBUG, 'Content.render')
 
@@ -66,8 +48,8 @@ class Content extends React.Component {
         <ContentTabs
           tabs={this.props.openTabs}
           selectedTab={this.props.selectedTab}
-          tabSelect={this.props.tabSelect}
-          tabClose={this.props.tabClose}
+          tabSelect={this.handleTabSelect}
+          tabClose={this.handleTabClose}
           renderTab={this.props.renderTab}
         />
         {this.props.openTabs.map(openTab => {
@@ -93,6 +75,9 @@ class Content extends React.Component {
 }
 
 export default _.flow(
-  connect(mapStateToProps, mapDispatchToProps),
-  withStyles(styles)
+  withStyles(styles),
+  AppController.withState(ownProps => ({
+    openTabs: AppController.getOpenTabs(ownProps.page),
+    selectedTab: AppController.getSelectedTab(ownProps.page)
+  }))
 )(Content)
diff --git a/openbis_ng_ui/src/js/components/common/form/LinkToObject.jsx b/openbis_ng_ui/src/js/components/common/form/LinkToObject.jsx
index 5d5e3c1a972..76c6903752c 100644
--- a/openbis_ng_ui/src/js/components/common/form/LinkToObject.jsx
+++ b/openbis_ng_ui/src/js/components/common/form/LinkToObject.jsx
@@ -1,10 +1,8 @@
-import _ from 'lodash'
 import React from 'react'
 import autoBind from 'auto-bind'
-import { connect } from 'react-redux'
 import { withStyles } from '@material-ui/core/styles'
+import AppController from '@src/js/components/AppController.js'
 import Link from '@material-ui/core/Link'
-import actions from '@src/js/store/actions/actions.js'
 
 const styles = () => ({
   link: {
@@ -12,14 +10,6 @@ const styles = () => ({
   }
 })
 
-function mapDispatchToProps(dispatch) {
-  return {
-    objectOpen: (page, objectType, objectId) => {
-      dispatch(actions.objectOpen(page, objectType, objectId))
-    }
-  }
-}
-
 class LinkToObject extends React.Component {
   constructor(props) {
     super(props)
@@ -28,7 +18,7 @@ class LinkToObject extends React.Component {
 
   handleClick() {
     const { page, object } = this.props
-    this.props.objectOpen(page, object.type, object.id)
+    AppController.objectOpen(page, object.type, object.id)
   }
 
   render() {
@@ -45,7 +35,4 @@ class LinkToObject extends React.Component {
   }
 }
 
-export default _.flow(
-  connect(null, mapDispatchToProps),
-  withStyles(styles)
-)(LinkToObject)
+export default withStyles(styles)(LinkToObject)
diff --git a/openbis_ng_ui/src/js/components/common/menu/Menu.jsx b/openbis_ng_ui/src/js/components/common/menu/Menu.jsx
index b0937a7ed1d..04ac927a3a5 100644
--- a/openbis_ng_ui/src/js/components/common/menu/Menu.jsx
+++ b/openbis_ng_ui/src/js/components/common/menu/Menu.jsx
@@ -1,3 +1,5 @@
+import _ from 'lodash'
+import autoBind from 'auto-bind'
 import React from 'react'
 import AppBar from '@material-ui/core/AppBar'
 import Toolbar from '@material-ui/core/Toolbar'
@@ -9,10 +11,8 @@ import SearchIcon from '@material-ui/icons/Search'
 import CloseIcon from '@material-ui/icons/Close'
 import LogoutIcon from '@material-ui/icons/PowerSettingsNew'
 import { alpha } from '@material-ui/core/styles/colorManipulator'
-import { connect } from 'react-redux'
 import { withStyles } from '@material-ui/core/styles'
-import actions from '@src/js/store/actions/actions.js'
-import selectors from '@src/js/store/selectors/selectors.js'
+import AppController from '@src/js/components/AppController.js'
 import Button from '@src/js/components/common/form/Button.jsx'
 import pages from '@src/js/common/consts/pages.js'
 import messages from '@src/js/common/messages.js'
@@ -57,48 +57,37 @@ const styles = theme => ({
   }
 })
 
-function mapStateToProps(state) {
-  return {
-    currentPage: selectors.getCurrentPage(state),
-    searchText: selectors.getSearch(state)
-  }
-}
-
-function mapDispatchToProps(dispatch, ownProps) {
-  return {
-    currentPageChange: (event, value) =>
-      dispatch(actions.currentPageChange(value)),
-    searchChange: value => dispatch(actions.searchChange(value)),
-    search: value => dispatch(actions.search(ownProps.page, value)),
-    logout: () => dispatch(actions.logout())
-  }
-}
-
 class Menu extends React.Component {
   constructor(props) {
     super(props)
+    autoBind(this)
     this.searchRef = React.createRef()
-    this.handleSearchChange = this.handleSearchChange.bind(this)
-    this.handleSearchKeyPress = this.handleSearchKeyPress.bind(this)
-    this.handleSearchClear = this.handleSearchClear.bind(this)
+  }
+
+  handlePageChange(event, value) {
+    AppController.pageChange(value)
   }
 
   handleSearchChange(event) {
-    this.props.searchChange(event.target.value)
+    AppController.searchChange(event.target.value)
   }
 
   handleSearchKeyPress(event) {
     if (event.key === 'Enter') {
-      this.props.search(this.props.searchText)
+      AppController.search(this.props.currentPage, this.props.searchText)
     }
   }
 
   handleSearchClear(event) {
     event.preventDefault()
-    this.props.searchChange('')
+    AppController.searchChange('')
     this.searchRef.current.focus()
   }
 
+  handleLogout() {
+    AppController.logout()
+  }
+
   render() {
     logger.log(logger.DEBUG, 'Menu.render')
 
@@ -109,7 +98,7 @@ class Menu extends React.Component {
         <Toolbar variant='dense' classes={{ root: classes.toolBar }}>
           <Tabs
             value={this.props.currentPage}
-            onChange={this.props.currentPageChange}
+            onChange={this.handlePageChange}
             classes={{ root: classes.tabs }}
           >
             <Tab value={pages.TYPES} label={messages.get(messages.TYPES)} />
@@ -118,7 +107,7 @@ class Menu extends React.Component {
           </Tabs>
           <TextField
             placeholder={messages.get(messages.SEARCH)}
-            value={searchText}
+            value={searchText || ''}
             onChange={this.handleSearchChange}
             onKeyPress={this.handleSearchKeyPress}
             InputProps={{
@@ -134,7 +123,7 @@ class Menu extends React.Component {
           <Button
             label={<LogoutIcon fontSize='small' />}
             type='final'
-            onClick={this.props.logout}
+            onClick={this.handleLogout}
           />
         </Toolbar>
       </AppBar>
@@ -168,7 +157,10 @@ class Menu extends React.Component {
   }
 }
 
-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(withStyles(styles)(Menu))
+export default _.flow(
+  withStyles(styles),
+  AppController.withState(() => ({
+    currentPage: AppController.getCurrentPage(),
+    searchText: AppController.getSearch()
+  }))
+)(Menu)
diff --git a/openbis_ng_ui/src/js/components/common/page/PageControllerChanged.js b/openbis_ng_ui/src/js/components/common/page/PageControllerChanged.js
index 22a47b70acc..a76054bde38 100644
--- a/openbis_ng_ui/src/js/components/common/page/PageControllerChanged.js
+++ b/openbis_ng_ui/src/js/components/common/page/PageControllerChanged.js
@@ -1,4 +1,4 @@
-import actions from '@src/js/store/actions/actions.js'
+import AppController from '@src/js/components/AppController.js'
 
 export default class PageControllerChanged {
   constructor(controller) {
@@ -16,8 +16,11 @@ export default class PageControllerChanged {
 
       const { id, type } = this.controller.getObject()
 
-      this.context.dispatch(
-        actions.objectChange(this.controller.getPage(), type, id, newChanged)
+      AppController.objectChange(
+        this.controller.getPage(),
+        type,
+        id,
+        newChanged
       )
     }
   }
diff --git a/openbis_ng_ui/src/js/components/common/page/PageControllerLoad.js b/openbis_ng_ui/src/js/components/common/page/PageControllerLoad.js
index 7f75c48df35..c1a9b1cb7fe 100644
--- a/openbis_ng_ui/src/js/components/common/page/PageControllerLoad.js
+++ b/openbis_ng_ui/src/js/components/common/page/PageControllerLoad.js
@@ -1,7 +1,7 @@
 import _ from 'lodash'
 import PageMode from '@src/js/components/common/page/PageMode.js'
 import FormValidator from '@src/js/components/common/form/FormValidator.js'
-import actions from '@src/js/store/actions/actions.js'
+import AppController from '@src/js/components/AppController.js'
 
 export default class PageControllerLoad {
   constructor(controller) {
@@ -44,7 +44,7 @@ export default class PageControllerLoad {
 
       await this.load(this.object, isNew)
     } catch (error) {
-      this.context.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     } finally {
       if (_.isFunction(this.controller.changed)) {
         this.controller.changed(false)
diff --git a/openbis_ng_ui/src/js/components/common/page/PageControllerSave.js b/openbis_ng_ui/src/js/components/common/page/PageControllerSave.js
index fddcf10eb13..141d85e5da2 100644
--- a/openbis_ng_ui/src/js/components/common/page/PageControllerSave.js
+++ b/openbis_ng_ui/src/js/components/common/page/PageControllerSave.js
@@ -1,6 +1,6 @@
 import _ from 'lodash'
 import FormValidator from '@src/js/components/common/form/FormValidator.js'
-import actions from '@src/js/store/actions/actions.js'
+import AppController from '@src/js/components/AppController.js'
 
 export default class PageControllerSave {
   constructor(controller) {
@@ -45,27 +45,23 @@ export default class PageControllerSave {
         await this.controller.load()
 
         if (oldObject.type === this.controller.getNewObjectType()) {
-          this.context.dispatch(
-            actions.objectCreate(
-              this.controller.getPage(),
-              oldObject.type,
-              oldObject.id,
-              newObject.type,
-              newObject.id
-            )
+          AppController.objectCreate(
+            this.controller.getPage(),
+            oldObject.type,
+            oldObject.id,
+            newObject.type,
+            newObject.id
           )
         } else if (oldObject.type === this.controller.getExistingObjectType()) {
-          this.context.dispatch(
-            actions.objectUpdate(
-              this.controller.getPage(),
-              oldObject.type,
-              oldObject.id
-            )
+          AppController.objectUpdate(
+            this.controller.getPage(),
+            oldObject.type,
+            oldObject.id
           )
         }
       }
     } catch (error) {
-      this.context.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     } finally {
       this.context.setState({
         loading: false
diff --git a/openbis_ng_ui/src/js/components/login/Login.jsx b/openbis_ng_ui/src/js/components/login/Login.jsx
index 5149eac54e8..7e417abf6d6 100644
--- a/openbis_ng_ui/src/js/components/login/Login.jsx
+++ b/openbis_ng_ui/src/js/components/login/Login.jsx
@@ -1,7 +1,6 @@
 import _ from 'lodash'
 import React from 'react'
 import { withStyles } from '@material-ui/core/styles'
-import { connect } from 'react-redux'
 import flow from 'lodash/flow'
 
 import Card from '@material-ui/core/Card'
@@ -14,8 +13,8 @@ import TextField from '@src/js/components/common/form/TextField.jsx'
 import SelectField from '@src/js/components/common/form/SelectField.jsx'
 import Button from '@src/js/components/common/form/Button.jsx'
 
+import AppController from '@src/js/components/AppController.js'
 import openbis from '@src/js/services/openbis.js'
-import actions from '@src/js/store/actions/actions.js'
 import messages from '@src/js/common/messages.js'
 import logger from '@src/js/common/logger.js'
 
@@ -42,16 +41,6 @@ const styles = theme => ({
   }
 })
 
-function mapStateToProps() {
-  return {}
-}
-
-function mapDispatchToProps(dispatch) {
-  return {
-    login: (user, password) => dispatch(actions.login(user, password))
-  }
-}
-
 const AUTHENTICATION_SERVICE_OPENBIS = 'openBIS'
 const AUTHENTICATION_SERVICE_SWITCH_AAI = 'SWITCHaai'
 
@@ -176,7 +165,7 @@ class WithLogin extends React.Component {
       },
       () => {
         if (this.validate(true)) {
-          this.props.login(this.state.user.value, this.state.password.value)
+          AppController.login(this.state.user.value, this.state.password.value)
         }
       }
     )
@@ -353,7 +342,4 @@ class WithLogin extends React.Component {
   }
 }
 
-export default flow(
-  connect(mapStateToProps, mapDispatchToProps),
-  withStyles(styles)
-)(WithLogin)
+export default flow(withStyles(styles))(WithLogin)
diff --git a/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
index 1b78036308c..2b31d38bfc8 100644
--- a/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
+++ b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
@@ -1,10 +1,10 @@
 import openbis from '@src/js/services/openbis.js'
-import actions from '@src/js/store/actions/actions.js'
 import pages from '@src/js/common/consts/pages.js'
 import objectType from '@src/js/common/consts/objectType.js'
 import objectOperation from '@src/js/common/consts/objectOperation.js'
 import ImportType from '@src/js/components/tools/form/import/ImportType.js'
 import BrowserController from '@src/js/components/common/browser/BrowserController.js'
+import AppController from '@src/js/components/AppController.js'
 import messages from '@src/js/common/messages.js'
 
 export default class ToolBrowserController extends BrowserController {
@@ -162,9 +162,7 @@ export default class ToolBrowserController extends BrowserController {
 
   doNodeAdd(node) {
     if (node && node.childrenType) {
-      this.context.dispatch(
-        actions.objectNew(this.getPage(), node.childrenType)
-      )
+      AppController.objectNew(this.getPage(), node.childrenType)
     }
   }
 
@@ -183,10 +181,10 @@ export default class ToolBrowserController extends BrowserController {
         return openbis.executeOperations(operations, options)
       })
       .then(() => {
-        this.context.dispatch(actions.objectDelete(this.getPage(), type, id))
+        AppController.objectDelete(this.getPage(), type, id)
       })
       .catch(error => {
-        this.context.dispatch(actions.errorChange(error))
+        AppController.errorChange(error)
       })
   }
 
diff --git a/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx b/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx
index 4fac0c10d78..137928647ef 100644
--- a/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx
+++ b/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx
@@ -9,12 +9,11 @@ import DateRangeField from '@src/js/components/common/form/DateRangeField.jsx'
 import FormUtil from '@src/js/components/common/form/FormUtil.js'
 import EntityType from '@src/js/components/common/dto/EntityType.js'
 import HistoryGridContentCell from '@src/js/components/tools/common/HistoryGridContentCell.jsx'
+import AppController from '@src/js/components/AppController.js'
 import openbis from '@src/js/services/openbis.js'
 import messages from '@src/js/common/messages.js'
 import date from '@src/js/common/date.js'
 import ids from '@src/js/common/consts/ids.js'
-import store from '@src/js/store/store.js'
-import actions from '@src/js/store/actions/actions.js'
 import logger from '@src/js/common/logger.js'
 
 class HistoryGrid extends React.PureComponent {
@@ -27,7 +26,7 @@ class HistoryGrid extends React.PureComponent {
     try {
       return await this.loadHistory(this.props.eventType, params)
     } catch (error) {
-      store.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     }
   }
 
diff --git a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js
index 8c851457490..95ed8d1a5e9 100644
--- a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js
+++ b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js
@@ -1,6 +1,6 @@
 import PageMode from '@src/js/components/common/page/PageMode.js'
+import AppController from '@src/js/components/AppController.js'
 import openbis from '@src/js/services/openbis.js'
-import actions from '@src/js/store/actions/actions.js'
 
 export default class PluginFormControllerEvaluate {
   constructor(controller) {
@@ -31,7 +31,7 @@ export default class PluginFormControllerEvaluate {
         }
       }))
     } catch (error) {
-      this.context.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     } finally {
       this.context.setState(state => ({
         ...state,
diff --git a/openbis_ng_ui/src/js/components/tools/form/query/QueryFormControllerExecute.js b/openbis_ng_ui/src/js/components/tools/form/query/QueryFormControllerExecute.js
index cd1a8120c37..a70ae909f60 100644
--- a/openbis_ng_ui/src/js/components/tools/form/query/QueryFormControllerExecute.js
+++ b/openbis_ng_ui/src/js/components/tools/form/query/QueryFormControllerExecute.js
@@ -1,6 +1,6 @@
 import PageMode from '@src/js/components/common/page/PageMode.js'
+import AppController from '@src/js/components/AppController.js'
 import openbis from '@src/js/services/openbis.js'
-import actions from '@src/js/store/actions/actions.js'
 
 export default class QueryFormControllerExecute {
   constructor(controller) {
@@ -41,7 +41,7 @@ export default class QueryFormControllerExecute {
         }
       }))
     } catch (error) {
-      this.context.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     } finally {
       this.context.setState(state => ({
         ...state,
diff --git a/openbis_ng_ui/src/js/components/tools/search/ToolSearch.jsx b/openbis_ng_ui/src/js/components/tools/search/ToolSearch.jsx
index 9d8012962ca..76dc82fba71 100644
--- a/openbis_ng_ui/src/js/components/tools/search/ToolSearch.jsx
+++ b/openbis_ng_ui/src/js/components/tools/search/ToolSearch.jsx
@@ -4,14 +4,13 @@ import React from 'react'
 import { withStyles } from '@material-ui/core/styles'
 import Container from '@src/js/components/common/form/Container.jsx'
 import GridContainer from '@src/js/components/common/grid/GridContainer.jsx'
+import AppController from '@src/js/components/AppController.js'
 import PluginsGrid from '@src/js/components/tools/common/PluginsGrid.jsx'
 import QueriesGrid from '@src/js/components/tools/common/QueriesGrid.jsx'
 import Message from '@src/js/components/common/form/Message.jsx'
 import FormUtil from '@src/js/components/common/form/FormUtil.js'
 import ids from '@src/js/common/consts/ids.js'
 import objectTypes from '@src/js/common/consts/objectType.js'
-import store from '@src/js/store/store.js'
-import actions from '@src/js/store/actions/actions.js'
 import openbis from '@src/js/services/openbis.js'
 import util from '@src/js/common/util.js'
 import messages from '@src/js/common/messages.js'
@@ -50,7 +49,7 @@ class ToolSearch extends React.Component {
         loaded: true
       }))
     } catch (error) {
-      store.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     }
   }
 
diff --git a/openbis_ng_ui/src/js/components/types/browser/TypeBrowserController.js b/openbis_ng_ui/src/js/components/types/browser/TypeBrowserController.js
index c0e8d02a680..e71fd11b98e 100644
--- a/openbis_ng_ui/src/js/components/types/browser/TypeBrowserController.js
+++ b/openbis_ng_ui/src/js/components/types/browser/TypeBrowserController.js
@@ -1,10 +1,10 @@
 import _ from 'lodash'
 import openbis from '@src/js/services/openbis.js'
-import actions from '@src/js/store/actions/actions.js'
 import pages from '@src/js/common/consts/pages.js'
 import objectType from '@src/js/common/consts/objectType.js'
 import objectOperation from '@src/js/common/consts/objectOperation.js'
 import BrowserController from '@src/js/components/common/browser/BrowserController.js'
+import AppController from '@src/js/components/AppController.js'
 import users from '@src/js/common/consts/users'
 import messages from '@src/js/common/messages.js'
 
@@ -147,9 +147,7 @@ export default class TypeBrowserController extends BrowserController {
 
   doNodeAdd(node) {
     if (node && node.childrenType) {
-      this.context.dispatch(
-        actions.objectNew(this.getPage(), node.childrenType)
-      )
+      AppController.objectNew(this.getPage(), node.childrenType)
     }
   }
 
@@ -166,9 +164,9 @@ export default class TypeBrowserController extends BrowserController {
       const options = new openbis.SynchronousOperationExecutionOptions()
       options.setExecuteInOrder(true)
       await openbis.executeOperations(operations, options)
-      this.context.dispatch(actions.objectDelete(this.getPage(), type, id))
+      AppController.objectDelete(this.getPage(), type, id)
     } catch (error) {
-      this.context.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     }
   }
 
diff --git a/openbis_ng_ui/src/js/components/types/form/entitytype/EntityTypeFormPreviewProperty.jsx b/openbis_ng_ui/src/js/components/types/form/entitytype/EntityTypeFormPreviewProperty.jsx
index d32609d2c0f..21921ee4320 100644
--- a/openbis_ng_ui/src/js/components/types/form/entitytype/EntityTypeFormPreviewProperty.jsx
+++ b/openbis_ng_ui/src/js/components/types/form/entitytype/EntityTypeFormPreviewProperty.jsx
@@ -9,8 +9,8 @@ import TextField from '@src/js/components/common/form/TextField.jsx'
 import SelectField from '@src/js/components/common/form/SelectField.jsx'
 import EntityTypeFormSelectionType from '@src/js/components/types/form/entitytype/EntityTypeFormSelectionType.js'
 import DataType from '@src/js/components/common/dto/DataType.js'
+import AppController from '@src/js/components/AppController.js'
 import openbis from '@src/js/services/openbis.js'
-import actions from '@src/js/store/actions/actions.js'
 import messages from '@src/js/common/messages.js'
 import logger from '@src/js/common/logger.js'
 import util from '@src/js/common/util.js'
@@ -161,7 +161,7 @@ class EntityTypeFormPreviewProperty extends React.PureComponent {
         }))
       })
       .catch(error => {
-        controller.getContext().dispatch(actions.errorChange(error))
+        AppController.errorChange(error)
       })
   }
 
@@ -177,7 +177,7 @@ class EntityTypeFormPreviewProperty extends React.PureComponent {
         }))
       })
       .catch(error => {
-        controller.getContext().dispatch(actions.errorChange(error))
+        AppController.errorChange(error)
       })
   }
 
@@ -194,7 +194,7 @@ class EntityTypeFormPreviewProperty extends React.PureComponent {
           }))
         })
         .catch(error => {
-          controller.getContext().dispatch(actions.errorChange(error))
+          AppController.errorChange(error)
         })
     } else {
       this.setState(() => ({
diff --git a/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx b/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
index be427619773..2c0c0e9eadb 100644
--- a/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
+++ b/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
@@ -3,6 +3,7 @@ import autoBind from 'auto-bind'
 import React from 'react'
 import { withStyles } from '@material-ui/core/styles'
 import Container from '@src/js/components/common/form/Container.jsx'
+import AppController from '@src/js/components/AppController.js'
 import GridContainer from '@src/js/components/common/grid/GridContainer.jsx'
 import EntityTypesGrid from '@src/js/components/types/common/EntityTypesGrid.jsx'
 import VocabularyTypesGrid from '@src/js/components/types/common/VocabularyTypesGrid.jsx'
@@ -10,8 +11,6 @@ import PropertyTypesGrid from '@src/js/components/types/common/PropertyTypesGrid
 import Message from '@src/js/components/common/form/Message.jsx'
 import ids from '@src/js/common/consts/ids.js'
 import objectTypes from '@src/js/common/consts/objectType.js'
-import store from '@src/js/store/store.js'
-import actions from '@src/js/store/actions/actions.js'
 import openbis from '@src/js/services/openbis.js'
 import util from '@src/js/common/util.js'
 import messages from '@src/js/common/messages.js'
@@ -53,7 +52,7 @@ class TypeSearch extends React.Component {
         loaded: true
       }))
     } catch (error) {
-      store.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     }
   }
 
diff --git a/openbis_ng_ui/src/js/components/users/browser/UserBrowserController.js b/openbis_ng_ui/src/js/components/users/browser/UserBrowserController.js
index c7d40660cac..93cfca99bac 100644
--- a/openbis_ng_ui/src/js/components/users/browser/UserBrowserController.js
+++ b/openbis_ng_ui/src/js/components/users/browser/UserBrowserController.js
@@ -1,9 +1,9 @@
 import openbis from '@src/js/services/openbis.js'
-import actions from '@src/js/store/actions/actions.js'
 import pages from '@src/js/common/consts/pages.js'
 import objectType from '@src/js/common/consts/objectType.js'
 import objectOperation from '@src/js/common/consts/objectOperation.js'
 import BrowserController from '@src/js/components/common/browser/BrowserController.js'
+import AppController from '@src/js/components/AppController.js'
 import messages from '@src/js/common/messages.js'
 
 export default class UserBrowserController extends BrowserController {
@@ -67,9 +67,7 @@ export default class UserBrowserController extends BrowserController {
 
   doNodeAdd(node) {
     if (node && node.childrenType) {
-      this.context.dispatch(
-        actions.objectNew(this.getPage(), node.childrenType)
-      )
+      AppController.objectNew(this.getPage(), node.childrenType)
     }
   }
 
@@ -88,7 +86,7 @@ export default class UserBrowserController extends BrowserController {
         return openbis.executeOperations(operations, options)
       })
       .then(() => {
-        this.context.dispatch(actions.objectDelete(this.getPage(), type, id))
+        AppController.objectDelete(this.getPage(), type, id)
       })
       .catch(error => {
         if (
@@ -96,15 +94,13 @@ export default class UserBrowserController extends BrowserController {
           error.message &&
           error.message.startsWith('Could not commit Hibernate transaction')
         ) {
-          this.context.dispatch(
-            actions.errorChange(
-              messages.get(
-                messages.USERS_WHO_REGISTERED_SOME_DATA_CANNOT_BE_REMOVED
-              )
+          AppController.errorChange(
+            messages.get(
+              messages.USERS_WHO_REGISTERED_SOME_DATA_CANNOT_BE_REMOVED
             )
           )
         } else {
-          this.context.dispatch(actions.errorChange(error))
+          AppController.errorChange(error)
         }
       })
   }
diff --git a/openbis_ng_ui/src/js/components/users/search/UserSearch.jsx b/openbis_ng_ui/src/js/components/users/search/UserSearch.jsx
index 6b3fe8d432d..3c934b3e15d 100644
--- a/openbis_ng_ui/src/js/components/users/search/UserSearch.jsx
+++ b/openbis_ng_ui/src/js/components/users/search/UserSearch.jsx
@@ -3,6 +3,7 @@ import autoBind from 'auto-bind'
 import React from 'react'
 import { withStyles } from '@material-ui/core/styles'
 import Container from '@src/js/components/common/form/Container.jsx'
+import AppController from '@src/js/components/AppController.js'
 import GridContainer from '@src/js/components/common/grid/GridContainer.jsx'
 import UsersGrid from '@src/js/components/users/common/UsersGrid.jsx'
 import UserGroupsGrid from '@src/js/components/users/common/UserGroupsGrid.jsx'
@@ -11,8 +12,6 @@ import Message from '@src/js/components/common/form/Message.jsx'
 import FormUtil from '@src/js/components/common/form/FormUtil.js'
 import ids from '@src/js/common/consts/ids.js'
 import objectTypes from '@src/js/common/consts/objectType.js'
-import store from '@src/js/store/store.js'
-import actions from '@src/js/store/actions/actions.js'
 import openbis from '@src/js/services/openbis.js'
 import util from '@src/js/common/util.js'
 import messages from '@src/js/common/messages.js'
@@ -55,7 +54,7 @@ class UserSearch extends React.Component {
         loaded: true
       }))
     } catch (error) {
-      store.dispatch(actions.errorChange(error))
+      AppController.errorChange(error)
     }
   }
 
diff --git a/openbis_ng_ui/src/js/store/history.js b/openbis_ng_ui/src/js/store/history.js
index a37412205f6..49da3566429 100644
--- a/openbis_ng_ui/src/js/store/history.js
+++ b/openbis_ng_ui/src/js/store/history.js
@@ -1,5 +1,5 @@
 import { createBrowserHistory } from 'history'
-import actions from '@src/js/store/actions/actions.js'
+import AppController from '@src/js/components/AppController.js'
 import routes from '@src/js/common/consts/routes.js'
 import url from '@src/js/common/url.js'
 
@@ -12,7 +12,7 @@ history.configure = store => {
     let route = routes.parse(location.pathname)
 
     if (route.path !== store.getState().route) {
-      store.dispatch(actions.routeChange(route.path, location.state))
+      AppController.routeChange(route.path, location.state)
     }
   })
 
-- 
GitLab