Repository: CSCfi/rems
Pull request #1549 (2019-08-28 13:03:43)

Use flash messages on application page
#1469

# Definition of Done / Review checklist

## Reviewability
- [x] link to issue

## Documentation
- [x] update changelog if necessary
- [x] add or update docstrings for namespaces and functions

## Testing
- [x] complex logic is unit tested
- [x] valuable features are integration / browser / acceptance tested automatically

## Accessibility
- [x] all icons have the aria-label attribute
- [x] all fields have a label
- [x] errors are linked to fields with aria-describedby
- [x] contrast is checked
- [x] conscious decision about where to move focus after an action

## Follow-up
- [x] new tasks are created for pending or remaining tasks
- [x] no critical TODOs left to implement



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fe812e88..88dc9d202 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,9 +15,6 @@ Changes since v2.4
 - The pdf button and API have been removed. We recommend using "print
   to pdf" in your browser.
 - The `start`, `end` and `expired` fields have been removed from licenses
-- API for creating catalogue item and its localizations has been changed.
-  There is now a single API call that is used to create both a catalogue
-  item and the localizations, namely, /api/catalogue-items/create.
 
 ### Additions
 - New field types: description, option, multiselect
@@ -42,8 +39,6 @@ Changes since v2.4
 - Extra pages (#472)
 - Full-text search for all application content (#873)
 - Creating a new application as a copy from an older application (#832)
-- Re-naming a catalogue item (#1507)
-- Add enable/disable and archive/unarchive buttons to 'View' pages (#1438)
 
 ### Enhancements
 - Improved version information in footer
@@ -52,6 +47,7 @@ Changes since v2.4
 - Hide language switcher when only one language configured
 - PDFs are now downloaded as \<application-id\>.pdf instead of pdf.pdf
 - Improved table performance: added a "show all rows" button for long tables
+- Modal popups (on application page) have been replaced with flash messages (#1469)
 
 ### Fixes
 - Entitlement API
diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql
index 6f722f484..da318eff6 100644
--- a/resources/sql/queries.sql
+++ b/resources/sql/queries.sql
@@ -134,12 +134,6 @@ INSERT INTO catalogue_item_localization
   (catid, langcode, title)
 VALUES (:id, :langcode, :title);
 
--- :name edit-catalogue-item-localization! :!
-UPDATE catalogue_item_localization
-SET (catid, langcode, title) = (:id, :langcode, :title)
-WHERE catid = :id
-  AND langcode = :langcode;
-
 -- :name get-form-templates :? :*
 SELECT
   id,
diff --git a/resources/translations/en.edn b/resources/translations/en.edn
index ef07daa4a..621b41106 100644
--- a/resources/translations/en.edn
+++ b/resources/translations/en.edn
@@ -57,7 +57,6 @@
                    :disable "Disable"
                    :display-old "Display old"
                    :edit "Edit"
-                   :edit-catalogue-item "Edit catalogue item"
                    :edit-form "Edit form"
                    :edit-workflow "Edit workflow"
                    :enable "Enable"
diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn
index 1a68f83b8..d861d30f6 100644
--- a/resources/translations/fi.edn
+++ b/resources/translations/fi.edn
@@ -57,7 +57,6 @@
                    :disable "Poista käytöstä"
                    :display-old "Näytä vanhat"
                    :edit "Muokkaa"
-                   :edit-catalogue-item "Muokkaa aineistoa"
                    :edit-form "Muokkaa lomaketta"
                    :edit-workflow "Muokkaa työvuota"
                    :enable "Ota käyttöön"
diff --git a/src/clj/rems/api/catalogue_items.clj b/src/clj/rems/api/catalogue_items.clj
index 4d152d36b..99f7551d7 100644
--- a/src/clj/rems/api/catalogue_items.clj
+++ b/src/clj/rems/api/catalogue_items.clj
@@ -17,18 +17,18 @@
   {:form s/Int
    :resid s/Int
    :wfid s/Int
-   :localizations {s/Keyword {:title s/Str}}
    (s/optional-key :enabled) s/Bool
    (s/optional-key :archived) s/Bool})
 
-(s/defschema EditCatalogueItemCommand
-  {:id s/Int
-   :localizations {s/Keyword {:title s/Str}}})
-
 (s/defschema CreateCatalogueItemResponse
   {:success s/Bool
    :id s/Int})
 
+(s/defschema CreateCatalogueItemLocalizationCommand
+  {:id s/Int
+   :langcode s/Str
+   :title s/Str})
+
 ;; TODO use declarative roles everywhere
 (def catalogue-items-api
   (context "/catalogue-items" []
@@ -69,16 +69,16 @@
       :return CreateCatalogueItemResponse
       (ok (catalogue/create-catalogue-item! command)))
 
-    (POST "/edit" []
-      :summary "Edit a catalogue item"
-      :roles #{:owner}
-      :body [command EditCatalogueItemCommand]
-      :return SuccessResponse
-      (ok (catalogue/edit-catalogue-item! command)))
-
     (PUT "/update" []
       :summary "Update catalogue item"
       :roles #{:owner}
       :body [command UpdateStateCommand]
       :return SuccessResponse
-      (ok (catalogue/update-catalogue-item! command)))))
+      (ok (catalogue/update-catalogue-item! command)))
+
+    (POST "/create-localization" []
+      :summary "Create a new catalogue item localization"
+      :roles #{:owner}
+      :body [command CreateCatalogueItemLocalizationCommand]
+      :return SuccessResponse
+      (ok (catalogue/create-catalogue-item-localization! command)))))
diff --git a/src/clj/rems/api/services/catalogue.clj b/src/clj/rems/api/services/catalogue.clj
index 2eb8f79c6..6216242b7 100644
--- a/src/clj/rems/api/services/catalogue.clj
+++ b/src/clj/rems/api/services/catalogue.clj
@@ -15,27 +15,16 @@
      :licenses (licenses/get-licenses {:wfid wfid
                                        :items [id]})}))
 
-(defn create-catalogue-item! [{:keys [localizations] :as command}]
-  (let [id (:id (db/create-catalogue-item! (select-keys command [:form :resid :wfid :enabled :archived])))
-        loc-ids
-        (doall
-         (for [[langcode localization] localizations]
-           (:id (db/create-catalogue-item-localization! {:id id
-                                                         :langcode (name langcode)
-                                                         :title (:title localization)}))))]
-    ;; Reset cache so that next call to get localizations will get these ones.
-    (catalogue/reset-cache!)
-    {:success (not (some nil? (cons id loc-ids)))
+(defn create-catalogue-item! [command]
+  (let [id (:id (db/create-catalogue-item! (select-keys command [:form :resid :wfid :enabled :archived])))]
+    {:success (not (nil? id))
      :id id}))
 
-(defn edit-catalogue-item! [{:keys [id localizations] :as command}]
-  (doseq [[langcode localization] localizations]
-    (db/edit-catalogue-item-localization! {:id id
-                                           :langcode (name langcode)
-                                           :title (:title localization)}))
-  ;; Reset cache so that next call to get localizations will get these ones.
-  (catalogue/reset-cache!)
-  {:success true})
+(defn create-catalogue-item-localization! [command]
+  (let [return {:success (not (nil? (:id (db/create-catalogue-item-localization! (select-keys command [:id :langcode :title])))))}]
+    ;; Reset cache so that next call to get localizations will get this one.
+    (catalogue/reset-cache!)
+    return))
 
 (defn update-catalogue-item! [command]
   (let [{:keys [form resource workflow licenses]} (dependencies-for-catalogue-item (:id command))
diff --git a/src/clj/rems/middleware.clj b/src/clj/rems/middleware.clj
index 899480631..89d3d5d39 100644
--- a/src/clj/rems/middleware.clj
+++ b/src/clj/rems/middleware.clj
@@ -174,9 +174,6 @@
       (log/info ">" (:request-method request) uri
                 "lang:" context/*lang*
                 "user:" context/*user*
-                (if (get-api-key request)
-                  "api-key"
-                  "")
                 "roles:" context/*roles*)
       (log/debug "session" (pr-str (:session request)))
       (when (seq (:form-params request))
diff --git a/src/cljs/rems/actions/add_licenses.cljs b/src/cljs/rems/actions/add_licenses.cljs
index 8ee884f61..60073110d 100644
--- a/src/cljs/rems/actions/add_licenses.cljs
+++ b/src/cljs/rems/actions/add_licenses.cljs
@@ -3,7 +3,7 @@
             [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
             [rems.dropdown :as dropdown]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text get-localized-title]]
             [rems.util :refer [fetch post!]]))
 
@@ -48,15 +48,17 @@
 (rf/reg-event-fx
  ::send-add-licenses
  (fn [_ [_ {:keys [application-id licenses comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/add-licenses))
-   (post! "/api/applications/add-licenses"
-          {:params {:application-id application-id
-                    :comment comment
-                    :licenses (map :id licenses)}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/add-licenses)]
+     (post! "/api/applications/add-licenses"
+            {:params {:application-id application-id
+                      :comment comment
+                      :licenses (map :id licenses)}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn add-licenses-action-button []
diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs
index c48bba24e..a7eb51ac2 100644
--- a/src/cljs/rems/actions/add_member.cljs
+++ b/src/cljs/rems/actions/add_member.cljs
@@ -3,7 +3,7 @@
             [rems.actions.action :refer [action-button action-form-view button-wrapper collapse-action-form]]
             [rems.atoms :refer [enrich-user]]
             [rems.dropdown :as dropdown]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [fetch post!]]))
 
@@ -39,14 +39,16 @@
 (rf/reg-event-fx
  ::send-add-member
  (fn [_ [_ {:keys [member application-id on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/add-member))
-   (post! "/api/applications/add-member"
-          {:params {:application-id application-id
-                    :member (select-keys member [:userid])}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/add-member)]
+     (post! "/api/applications/add-member"
+            {:params {:application-id application-id
+                      :member (select-keys member [:userid])}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn add-member-action-button []
diff --git a/src/cljs/rems/actions/approve_reject.cljs b/src/cljs/rems/actions/approve_reject.cljs
index 5173c01d5..1dbe6d089 100644
--- a/src/cljs/rems/actions/approve_reject.cljs
+++ b/src/cljs/rems/actions/approve_reject.cljs
@@ -1,7 +1,7 @@
 (ns rems.actions.approve-reject
   (:require [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [post!]]))
 
@@ -18,27 +18,31 @@
 (rf/reg-event-fx
  ::send-approve
  (fn [_ [_ {:keys [application-id comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/approve))
-   (post! "/api/applications/approve"
-          {:params {:application-id application-id
-                    :comment comment}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/approve)]
+     (post! "/api/applications/approve"
+            {:params {:application-id application-id
+                      :comment comment}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (rf/reg-event-fx
  ::send-reject
  (fn [_ [_ {:keys [application-id comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/reject))
-   (post! "/api/applications/reject"
-          {:params {:application-id application-id
-                    :comment comment}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/reject)]
+     (post! "/api/applications/reject"
+            {:params {:application-id application-id
+                      :comment comment}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn approve-reject-action-button []
diff --git a/src/cljs/rems/actions/close.cljs b/src/cljs/rems/actions/close.cljs
index 49108d486..40eff73f5 100644
--- a/src/cljs/rems/actions/close.cljs
+++ b/src/cljs/rems/actions/close.cljs
@@ -1,7 +1,7 @@
 (ns rems.actions.close
   (:require [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper]]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [post!]]))
 
@@ -16,12 +16,12 @@
 (rf/reg-event-fx
  ::send-close
  (fn [_ [_ {:keys [application-id comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/close))
-   (post! "/api/applications/close"
-          {:params {:application-id application-id
-                    :comment comment}
-           :handler (partial status-modal/common-success-handler! on-finished)
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/close)]
+     (post! "/api/applications/close"
+            {:params {:application-id application-id
+                      :comment comment}
+             :handler (flash-message/default-success-handler description on-finished)
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (def ^:private action-form-id "close")
diff --git a/src/cljs/rems/actions/decide.cljs b/src/cljs/rems/actions/decide.cljs
index 8c53d10d2..536b2d11b 100644
--- a/src/cljs/rems/actions/decide.cljs
+++ b/src/cljs/rems/actions/decide.cljs
@@ -1,7 +1,7 @@
 (ns rems.actions.decide
   (:require [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [post!]]))
 
@@ -18,15 +18,17 @@
 (rf/reg-event-fx
  ::send-decide
  (fn [_ [_ {:keys [application-id comment decision on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/decide))
-   (post! "/api/applications/decide"
-          {:params {:application-id application-id
-                    :decision decision
-                    :comment comment}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/decide)]
+     (post! "/api/applications/decide"
+            {:params {:application-id application-id
+                      :decision decision
+                      :comment comment}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn decide-action-button []
diff --git a/src/cljs/rems/actions/remark.cljs b/src/cljs/rems/actions/remark.cljs
index eaed8c1da..05154d9d1 100644
--- a/src/cljs/rems/actions/remark.cljs
+++ b/src/cljs/rems/actions/remark.cljs
@@ -1,7 +1,7 @@
 (ns rems.actions.remark
   (:require [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [post!]]))
 
@@ -23,15 +23,17 @@
 (rf/reg-event-fx
  ::send-remark
  (fn [{:keys [db]} [_ {:keys [application-id on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/remark))
-   (post! "/api/applications/remark"
-          {:params {:application-id application-id
-                    :comment (::comment db)
-                    :public (::public db)}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/remark)]
+     (post! "/api/applications/remark"
+            {:params {:application-id application-id
+                      :comment (::comment db)
+                      :public (::public db)}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn remark-action-button []
diff --git a/src/cljs/rems/actions/remove_member.cljs b/src/cljs/rems/actions/remove_member.cljs
index 3fb962734..019b1c4e6 100644
--- a/src/cljs/rems/actions/remove_member.cljs
+++ b/src/cljs/rems/actions/remove_member.cljs
@@ -1,7 +1,7 @@
 (ns rems.actions.remove-member
   (:require [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [post!]]))
 
@@ -23,19 +23,21 @@
 (rf/reg-event-fx
  ::remove-member
  (fn [_ [_ {:keys [collapse-id application-id member comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/remove-member))
-   (post! (if (:userid member)
-            "/api/applications/remove-member"
-            "/api/applications/uninvite-member")
-          {:params {:application-id application-id
-                    :member (if (:userid member)
-                              (select-keys member [:userid])
-                              (select-keys member [:name :email]))
-                    :comment comment}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form collapse-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/remove-member)]
+     (post! (if (:userid member)
+              "/api/applications/remove-member"
+              "/api/applications/uninvite-member")
+            {:params {:application-id application-id
+                      :member (if (:userid member)
+                                (select-keys member [:userid])
+                                (select-keys member [:name :email]))
+                      :comment comment}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form collapse-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn remove-member-action-button [element-id]
diff --git a/src/cljs/rems/actions/request_decision.cljs b/src/cljs/rems/actions/request_decision.cljs
index 6ae4b2fef..ee9208f19 100644
--- a/src/cljs/rems/actions/request_decision.cljs
+++ b/src/cljs/rems/actions/request_decision.cljs
@@ -3,7 +3,7 @@
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
             [rems.atoms :refer [enrich-user]]
             [rems.dropdown :as dropdown]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [fetch post!]]))
 
@@ -41,15 +41,17 @@
 (rf/reg-event-fx
  ::send-request-decision
  (fn [_ [_ {:keys [deciders application-id comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/request-decision))
-   (post! "/api/applications/request-decision"
-          {:params {:application-id application-id
-                    :comment comment
-                    :deciders (map :userid deciders)}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/request-decision)]
+     (post! "/api/applications/request-decision"
+            {:params {:application-id application-id
+                      :comment comment
+                      :deciders (map :userid deciders)}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn request-decision-action-button []
diff --git a/src/cljs/rems/actions/request_review.cljs b/src/cljs/rems/actions/request_review.cljs
index 193ab81fa..ac30fb64d 100644
--- a/src/cljs/rems/actions/request_review.cljs
+++ b/src/cljs/rems/actions/request_review.cljs
@@ -3,7 +3,7 @@
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
             [rems.atoms :refer [enrich-user]]
             [rems.dropdown :as dropdown]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [fetch post!]]))
 
@@ -43,15 +43,17 @@
 (rf/reg-event-fx
  ::send-request-review
  (fn [_ [_ {:keys [application-id reviewers comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/request-review))
-   (post! "/api/applications/request-comment"
-          {:params {:application-id application-id
-                    :comment comment
-                    :commenters (map :userid reviewers)}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/request-review)]
+     (post! "/api/applications/request-comment"
+            {:params {:application-id application-id
+                      :comment comment
+                      :commenters (map :userid reviewers)}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn request-review-action-button []
diff --git a/src/cljs/rems/actions/return_action.cljs b/src/cljs/rems/actions/return_action.cljs
index 11d32d77c..becc9be73 100644
--- a/src/cljs/rems/actions/return_action.cljs
+++ b/src/cljs/rems/actions/return_action.cljs
@@ -1,7 +1,7 @@
 (ns rems.actions.return-action
   (:require [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [post!]]))
 
@@ -18,15 +18,16 @@
 (rf/reg-event-fx
  ::send-return
  (fn [_ [_ {:keys [application-id comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/return))
-   (post! "/api/applications/return"
-          {:params {:application-id application-id
-                    :comment comment}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    ;; not strictly necessary since the form disappears:
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/return)]
+     (post! "/api/applications/return"
+            {:params {:application-id application-id
+                      :comment comment}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn return-action-button []
diff --git a/src/cljs/rems/actions/review.cljs b/src/cljs/rems/actions/review.cljs
index cc8b9d9ac..08b66837a 100644
--- a/src/cljs/rems/actions/review.cljs
+++ b/src/cljs/rems/actions/review.cljs
@@ -1,7 +1,7 @@
 (ns rems.actions.review
   (:require [re-frame.core :as rf]
             [rems.actions.action :refer [action-button action-form-view action-comment button-wrapper collapse-action-form]]
-            [rems.status-modal :as status-modal]
+            [rems.flash-message :as flash-message]
             [rems.text :refer [text]]
             [rems.util :refer [post!]]))
 
@@ -18,14 +18,16 @@
 (rf/reg-event-fx
  ::send-review
  (fn [_ [_ {:keys [application-id comment on-finished]}]]
-   (status-modal/common-pending-handler! (text :t.actions/review))
-   (post! "/api/applications/comment"
-          {:params {:application-id application-id
-                    :comment comment}
-           :handler (partial status-modal/common-success-handler! (fn [_]
-                                                                    (collapse-action-form action-form-id)
-                                                                    (on-finished)))
-           :error-handler status-modal/common-error-handler!})
+   (let [description (text :t.actions/review)]
+     (post! "/api/applications/comment"
+            {:params {:application-id application-id
+                      :comment comment}
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [_]
+                         (collapse-action-form action-form-id)
+                         (on-finished)))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn review-action-button []
diff --git a/src/cljs/rems/administration/catalogue_item.cljs b/src/cljs/rems/administration/catalogue_item.cljs
index 138961574..7f27bef5b 100644
--- a/src/cljs/rems/administration/catalogue_item.cljs
+++ b/src/cljs/rems/administration/catalogue_item.cljs
@@ -3,7 +3,6 @@
             [re-frame.core :as rf]
             [rems.administration.administration :refer [administration-navigator-container]]
             [rems.administration.components :refer [inline-info-field]]
-            [rems.administration.status-flags :as status-flags]
             [rems.atoms :as atoms :refer [info-field readonly-checkbox document-title]]
             [rems.collapsible :as collapsible]
             [rems.spinner :as spinner]
@@ -37,11 +36,6 @@
    "/#/administration/catalogue-items"
    (text :t.administration/back)])
 
-(defn- edit-button [id]
-  [atoms/link {:class "btn btn-secondary"}
-   (str "/#/administration/edit-catalogue-item/" id)
-   (text :t.administration/edit)])
-
 (defn catalogue-item-view [catalogue-item language]
   [:div.spaced-vertically-3
    [collapsible/component
@@ -68,13 +62,8 @@
                        (:form-name catalogue-item)]]
                      [inline-info-field (text :t.administration/start) (localize-time (:start catalogue-item))]
                      [inline-info-field (text :t.administration/end) (localize-time (:end catalogue-item))]
-                     [inline-info-field (text :t.administration/active) [readonly-checkbox (status-flags/active? catalogue-item)]]]))}]
-   (let [id (:id catalogue-item)]
-     [:div.col.commands
-      [back-button]
-      [edit-button id]
-      [status-flags/enabled-toggle catalogue-item #(rf/dispatch [:rems.administration.catalogue-items/update-catalogue-item %1 %2 [::enter-page id]])]
-      [status-flags/archived-toggle catalogue-item #(rf/dispatch [:rems.administration.catalogue-items/update-catalogue-item %1 %2 [::enter-page id]])]])])
+                     [inline-info-field (text :t.administration/active) [readonly-checkbox (not (:expired catalogue-item))]]]))}]
+   [:div.col.commands [back-button]]])
 
 (defn catalogue-item-page []
   (let [catalogue-item (rf/subscribe [::catalogue-item])
diff --git a/src/cljs/rems/administration/catalogue_items.cljs b/src/cljs/rems/administration/catalogue_items.cljs
index 9eca5c7c4..221d97667 100644
--- a/src/cljs/rems/administration/catalogue_items.cljs
+++ b/src/cljs/rems/administration/catalogue_items.cljs
@@ -39,11 +39,11 @@
 
 (rf/reg-event-fx
  ::update-catalogue-item
- (fn [_ [_ item description dispatch-on-finished]]
+ (fn [_ [_ item description]]
    (status-modal/common-pending-handler! description)
    (put! "/api/catalogue-items/update"
          {:params (select-keys item [:id :enabled :archived])
-          :handler (partial status-flags/common-update-handler! #(rf/dispatch dispatch-on-finished))
+          :handler (partial status-flags/common-update-handler! #(rf/dispatch [::fetch-catalogue]))
           :error-handler status-modal/common-error-handler!})
    {}))
 
@@ -64,11 +64,6 @@
    (str "/#/administration/catalogue-items/" catalogue-item-id)
    (text :t.administration/view)])
 
-(defn- to-edit-catalogue-item [catalogue-item-id]
-  [atoms/link {:class "btn btn-primary"}
-   (str "/#/administration/edit-catalogue-item/" catalogue-item-id)
-   (text :t.administration/edit)])
-
 (rf/reg-sub
  ::catalogue-table-rows
  (fn [_ _]
@@ -102,15 +97,15 @@
            :end (let [value (:end item)]
                   {:value value
                    :display-value (localize-time value)})
-           :active (let [checked? (status-flags/active? item)]
+           ;; TODO: active means not-expired currently. it should maybe mean (and not-expired enabled not-archived)
+           :active (let [checked? (not (:expired item))]
                      {:td [:td.active
                            [readonly-checkbox checked?]]
                       :sort-value (if checked? 1 2)})
            :commands {:td [:td.commands
                            [to-catalogue-item (:id item)]
-                           [to-edit-catalogue-item (:id item)]
-                           [status-flags/enabled-toggle item #(rf/dispatch [::update-catalogue-item %1 %2 [::fetch-catalogue]])]
-                           [status-flags/archived-toggle item #(rf/dispatch [::update-catalogue-item %1 %2 [::fetch-catalogue]])]]}})
+                           [status-flags/enabled-toggle item #(rf/dispatch [::update-catalogue-item %1 %2])]
+                           [status-flags/archived-toggle item #(rf/dispatch [::update-catalogue-item %1 %2])]]}})
         catalogue)))
 
 (defn- catalogue-list []
diff --git a/src/cljs/rems/administration/create_catalogue_item.cljs b/src/cljs/rems/administration/create_catalogue_item.cljs
index fbaaa1cb8..bf481629f 100644
--- a/src/cljs/rems/administration/create_catalogue_item.cljs
+++ b/src/cljs/rems/administration/create_catalogue_item.cljs
@@ -1,6 +1,5 @@
 (ns rems.administration.create-catalogue-item
   (:require [clojure.string :as str]
-            [medley.core :refer [map-vals]]
             [re-frame.core :as rf]
             [rems.administration.administration :refer [administration-navigator-container]]
             [rems.administration.components :refer [text-field]]
@@ -10,7 +9,7 @@
             [rems.spinner :as spinner]
             [rems.status-modal :as status-modal]
             [rems.text :refer [text]]
-            [rems.util :refer [dispatch! fetch post! put!]]))
+            [rems.util :refer [dispatch! fetch post!]]))
 
 (defn- update-loading [db]
   (let [progress (::loading-progress db)]
@@ -20,89 +19,64 @@
 
 (rf/reg-event-fx
  ::enter-page
- (fn [{:keys [db]} [_ catalogue-item-id]]
+ (fn [{:keys [db]}]
    {:db (-> (dissoc db ::form)
-            (assoc ::catalogue-item-id catalogue-item-id
-                   ::loading-catalogue-item? (not (nil? catalogue-item-id))
-                   ::loading? true
+            (assoc ::loading? true
                    ::loading-progress 0))
-    ::fetch-catalogue-item catalogue-item-id
     ::fetch-workflows nil
     ::fetch-resources nil
     ::fetch-forms nil}))
 
-(rf/reg-sub ::catalogue-item (fn [db _] (::catalogue-item db)))
-(rf/reg-sub ::editing? (fn [db _] (not (nil? (::catalogue-item-id db)))))
 (rf/reg-sub ::loading? (fn [db _] (::loading? db)))
 
 (rf/reg-sub ::form (fn [db _] (::form db)))
 (rf/reg-event-db ::set-form-field (fn [db [_ keys value]] (assoc-in db (concat [::form] keys) value)))
 
-(rf/reg-sub ::selected-workflow-id (fn [db _] (get-in db [::form :workflow-id])))
-(rf/reg-event-db ::set-selected-workflow-id (fn [db [_ workflow-id]] (assoc-in db [::form :workflow-id] workflow-id)))
+(rf/reg-sub ::selected-workflow (fn [db _] (get-in db [::form :workflow])))
+(rf/reg-event-db ::set-selected-workflow (fn [db [_ workflow]] (assoc-in db [::form :workflow] workflow)))
 
-(rf/reg-sub ::selected-resource-id (fn [db _] (get-in db [::form :resource-id])))
-(rf/reg-event-db ::set-selected-resource-id (fn [db [_ resource-id]] (assoc-in db [::form :resource-id] resource-id)))
+(rf/reg-sub ::selected-resource (fn [db _] (get-in db [::form :resource])))
+(rf/reg-event-db ::set-selected-resource (fn [db [_ resource]] (assoc-in db [::form :resource] resource)))
 
-(rf/reg-sub ::selected-form-id (fn [db _] (get-in db [::form :form-id])))
-(rf/reg-event-db ::set-selected-form-id (fn [db [_ form-id]] (assoc-in db [::form :form-id] form-id)))
+(rf/reg-sub ::selected-form (fn [db _] (get-in db [::form :form])))
+(rf/reg-event-db ::set-selected-form (fn [db [_ form]] (assoc-in db [::form :form] form)))
 
 (defn- valid-localization? [localization]
-  (not (str/blank? (:title localization))))
+  (and (not (str/blank? (:langcode localization)))
+       (not (str/blank? (:title localization)))))
 
-(defn- valid-request? [request languages]
+(defn- valid-request? [request]
   (and (number? (:wfid request))
        (number? (:resid request))
        (number? (:form request))
-       (= (set languages)
-          (set (keys (:localizations request))))
-       (every? valid-localization? (vals (:localizations request)))))
+       (every? valid-localization? (:localizations request))))
 
 (defn build-request [form languages]
-  (let [request {:wfid (:workflow-id form)
-                 :resid (:resource-id form)
-                 :form (:form-id form)
-                 :localizations (map-vals (fn [title] {:title title})
-                                          (:title form))}]
-    (when (valid-request? request languages)
+  (let [request {:wfid (get-in form [:workflow :id])
+                 :resid (get-in form [:resource :id])
+                 :form (get-in form [:form :id])
+                 :localizations (for [language languages]
+                                  {:langcode (name language)
+                                   :title (get (:title form) language)})}]
+    (when (valid-request? request)
       request)))
 
-(defn- page-title [editing?]
-  (if editing?
-    (text :t.administration/edit-catalogue-item)
-    (text :t.administration/create-catalogue-item)))
-
 (defn- create-catalogue-item! [_ [_ request]]
   (status-modal/common-pending-handler! (text :t.administration/create-catalogue-item))
-  (post! "/api/catalogue-items/create"
-         {:params
-          (-> request
-              ;; create disabled catalogue items by default
-              (assoc :enabled false))
-          :handler
-          (partial status-modal/common-success-handler!
-                   (fn [response]
-                     (dispatch! (str "#/administration/catalogue-items/"
-                                     (:id response)))))
-          :error-handler status-modal/common-error-handler!})
-  {})
-
-(defn- edit-catalogue-item! [{:keys [db]} [_ request]]
-  (let [id (db ::catalogue-item-id)]
-    (status-modal/common-pending-handler! (text :t.administration/edit-catalogue-item))
-    (post! "/api/catalogue-items/edit"
-           {:params {:id id
-                     :localizations (:localizations request)}
-            :handler
-            (partial status-modal/common-success-handler!
-                     (fn [response]
-                       (dispatch! (str "#/administration/catalogue-items/"
-                                       id))))
-            :error-handler status-modal/common-error-handler!}))
+  (post! "/api/catalogue-items/create" {:params (-> request
+                                                    (assoc :enabled false) ;; create disabled catalogue items by default
+                                                    (dissoc :localizations))
+                                        :handler (partial status-modal/common-success-handler!
+                                                          (fn [response]
+                                                            (doseq [localization (:localizations request)]
+                                                              ;; XXX: no error handling & possible partial failure due to how cumbersome to old API is to use
+                                                              (post! "/api/catalogue-items/create-localization" {:params (assoc localization :id (:id response))}))
+                                                            (dispatch! (str "#/administration/catalogue-items/" (:id response)))))
+                                        :error-handler status-modal/common-error-handler!})
   {})
 
 (rf/reg-event-fx ::create-catalogue-item create-catalogue-item!)
-(rf/reg-event-fx ::edit-catalogue-item edit-catalogue-item!)
+
 
 (defn- fetch-workflows []
   (fetch "/api/workflows" {:handler #(rf/dispatch [::fetch-workflows-result %])}))
@@ -145,25 +119,6 @@
 (rf/reg-sub ::forms (fn [db _] (::forms db)))
 
 
-(defn- fetch-catalogue-item [id]
-  (fetch (str "/api/catalogue-items/" id)
-         {:handler #(rf/dispatch [::fetch-catalogue-item-result %])}))
-
-(rf/reg-fx
- ::fetch-catalogue-item
- (fn [id]
-   (when id (fetch-catalogue-item id))))
-
-(rf/reg-event-db
- ::fetch-catalogue-item-result
- (fn [db [_ {:keys [wfid resource-id formid localizations] :as catalogue-item}]]
-   (-> db
-       (assoc ::form {:workflow-id wfid
-                      :resource-id resource-id
-                      :form-id formid
-                      :title (map-vals :title localizations)})
-       (dissoc ::loading-catalogue-item?))))
-
 ;;;; UI
 
 (def ^:private context
@@ -182,87 +137,68 @@
 
 (defn- catalogue-item-workflow-field []
   (let [workflows @(rf/subscribe [::workflows])
-        editing? @(rf/subscribe [::editing?])
-        catalogue-item @(rf/subscribe [::catalogue-item])
-        selected-workflow-id @(rf/subscribe [::selected-workflow-id])
-        item-selected? #(= (:id %) selected-workflow-id)]
+        selected-workflow @(rf/subscribe [::selected-workflow])]
     [:div.form-group
      [:label {:for workflow-dropdown-id} (text :t.create-catalogue-item/workflow-selection)]
      [dropdown/dropdown
       {:id workflow-dropdown-id
        :items workflows
-       :item-disabled? (if editing?
-                         (comp not item-selected?)
-                         (constantly false))
        :item-key :id
        :item-label :title
-       :item-selected? item-selected?
-       :on-change #(rf/dispatch [::set-selected-workflow-id (:id %)])}]]))
+       :item-selected? #(= (:id %) (:id selected-workflow))
+       :on-change #(rf/dispatch [::set-selected-workflow %])}]]))
 
 (defn- catalogue-item-resource-field []
   (let [resources @(rf/subscribe [::resources])
-        editing? @(rf/subscribe [::editing?])
-        selected-resource-id @(rf/subscribe [::selected-resource-id])
-        item-selected? #(= (:id %) selected-resource-id)]
+        selected-resource @(rf/subscribe [::selected-resource])]
     [:div.form-group
      [:label {:for resource-dropdown-id} (text :t.create-catalogue-item/resource-selection)]
      [dropdown/dropdown
       {:id resource-dropdown-id
        :items resources
-       :item-disabled? (if editing?
-                         (comp not item-selected?)
-                         (constantly false))
        :item-key :id
        :item-label :resid
-       :item-selected? item-selected?
-       :on-change #(rf/dispatch [::set-selected-resource-id (:id %)])}]]))
+       :item-selected? #(= (:id %) (:id selected-resource))
+       :on-change #(rf/dispatch [::set-selected-resource %])}]]))
 
 (defn- catalogue-item-form-field []
   (let [forms @(rf/subscribe [::forms])
-        editing? @(rf/subscribe [::editing?])
-        selected-form-id @(rf/subscribe [::selected-form-id])
-        item-selected? #(= (:form/id %) selected-form-id)]
+        selected-form @(rf/subscribe [::selected-form])]
     [:div.form-group
      [:label {:for form-dropdown-id} (text :t.create-catalogue-item/form-selection)]
      [dropdown/dropdown
       {:id form-dropdown-id
        :items forms
-       :item-disabled? (if editing?
-                         (comp not item-selected?)
-                         (constantly false))
        :item-key :id
        :item-label :form/title
-       :item-selected? item-selected?
-       :on-change #(rf/dispatch [::set-selected-form-id (:id %)])}]]))
+       :item-selected? #(= (:form/id %) (:id selected-form))
+       :on-change #(rf/dispatch [::set-selected-form %])}]]))
 
 (defn- cancel-button []
   [atoms/link {:class "btn btn-secondary"}
    "/#/administration/catalogue-items"
    (text :t.administration/cancel)])
 
-(defn- save-catalogue-item-button [form languages editing?]
+(defn- save-catalogue-item-button [form languages on-click]
   (let [request (build-request form languages)]
     [:button.btn.btn-primary
      {:type :button
       :on-click (fn []
                   (rf/dispatch [:rems.spa/user-triggered-navigation])
-                  (if editing?
-                    (rf/dispatch [::edit-catalogue-item request])
-                    (rf/dispatch [::create-catalogue-item request])))
+                  (on-click request))
       :disabled (nil? request)}
      (text :t.administration/save)]))
 
 (defn create-catalogue-item-page []
   (let [languages @(rf/subscribe [:languages])
-        editing? @(rf/subscribe [::editing?])
         loading? @(rf/subscribe [::loading?])
         form @(rf/subscribe [::form])]
     [:div
      [administration-navigator-container]
-     [document-title (page-title editing?)]
+     [document-title (text :t.administration/create-catalogue-item)]
      [collapsible/component
       {:id "create-catalogue-item"
-       :title (page-title editing?)
+       :title (text :t.administration/create-catalogue-item)
        :always [:div
                 (if loading?
                   [:div#catalogue-item-loader [spinner/big]]
@@ -275,4 +211,4 @@
 
                    [:div.col.commands
                     [cancel-button]
-                    [save-catalogue-item-button form languages editing?]]])]}]]))
+                    [save-catalogue-item-button form languages #(rf/dispatch [::create-catalogue-item %])]]])]}]]))
diff --git a/src/cljs/rems/administration/form.cljs b/src/cljs/rems/administration/form.cljs
index adf2f2886..fd1f61bfc 100644
--- a/src/cljs/rems/administration/form.cljs
+++ b/src/cljs/rems/administration/form.cljs
@@ -73,14 +73,11 @@
               [inline-info-field (text :t.administration/title) (:form/title form)]
               [inline-info-field (text :t.administration/start) (localize-time (:start form))]
               [inline-info-field (text :t.administration/end) (localize-time (:end form))]
-              [inline-info-field (text :t.administration/active) [readonly-checkbox (status-flags/active? form)]]]}]
-   (let [id (:form/id form)]
-     [:div.col.commands
-      [back-button]
-      [edit-button id]
-      [copy-as-new-button id]
-      [status-flags/enabled-toggle form #(rf/dispatch [:rems.administration.forms/update-form %1 %2 [::enter-page id]])]
-      [status-flags/archived-toggle form #(rf/dispatch [:rems.administration.forms/update-form %1 %2 [::enter-page id]])]])
+              [inline-info-field (text :t.administration/active) [readonly-checkbox (not (:expired form))]]]}]
+   [:div.col.commands
+    [back-button]
+    [edit-button (:form/id form)]
+    [copy-as-new-button (:form/id form)]]
    [form-preview form]])
 ;; TODO Do we support form licenses?
 
diff --git a/src/cljs/rems/administration/forms.cljs b/src/cljs/rems/administration/forms.cljs
index 1a049af3b..2b2aec656 100644
--- a/src/cljs/rems/administration/forms.cljs
+++ b/src/cljs/rems/administration/forms.cljs
@@ -37,13 +37,13 @@
 
 (rf/reg-event-fx
  ::update-form
- (fn [_ [_ form description dispatch-on-finished]]
+ (fn [_ [_ form description]]
    (status-modal/common-pending-handler! description)
    (put! "/api/forms/update"
          {:params {:id (:form/id form)
                    :enabled (:enabled form)
                    :archived (:archived form)}
-          :handler (partial status-flags/common-update-handler! #(rf/dispatch dispatch-on-finished))
+          :handler (partial status-flags/common-update-handler! #(rf/dispatch [::fetch-forms]))
           :error-handler status-modal/common-error-handler!})
    {}))
 
@@ -93,7 +93,7 @@
            :end (let [value (:end form)]
                   {:value value
                    :display-value (localize-time value)})
-           :active (let [checked? (status-flags/active? form)]
+           :active (let [checked? (not (:expired form))]
                      {:td [:td.active
                            [readonly-checkbox checked?]]
                       :sort-value (if checked? 1 2)})
@@ -101,8 +101,8 @@
                            [to-view-form form]
                            [to-edit-form form]
                            [copy-as-new-form form]
-                           [status-flags/enabled-toggle form #(rf/dispatch [::update-form %1 %2 [::fetch-forms]])]
-                           [status-flags/archived-toggle form #(rf/dispatch [::update-form %1 %2 [::fetch-forms]])]]}})
+                           [status-flags/enabled-toggle form #(rf/dispatch [::update-form %1 %2])]
+                           [status-flags/archived-toggle form #(rf/dispatch [::update-form %1 %2])]]}})
         forms)))
 
 (defn- forms-list []
diff --git a/src/cljs/rems/administration/license.cljs b/src/cljs/rems/administration/license.cljs
index 75cf3c9ba..e155c43d8 100644
--- a/src/cljs/rems/administration/license.cljs
+++ b/src/cljs/rems/administration/license.cljs
@@ -3,7 +3,6 @@
             [re-frame.core :as rf]
             [rems.administration.administration :refer [administration-navigator-container]]
             [rems.administration.components :refer [inline-info-field]]
-            [rems.administration.status-flags :as status-flags]
             [rems.atoms :as atoms :refer [attachment-link external-link readonly-checkbox document-title]]
             [rems.collapsible :as collapsible]
             [rems.spinner :as spinner]
@@ -72,12 +71,8 @@
                            {:box? false}])))
                     [[inline-info-field (text :t.administration/start) (localize-time (:start license))]
                      [inline-info-field (text :t.administration/end) (localize-time (:end license))]
-                     [inline-info-field (text :t.administration/active) [readonly-checkbox (status-flags/active? license)]]]))}]
-   (let [id (:id license)]
-     [:div.col.commands
-      [back-button]
-      [status-flags/enabled-toggle license #(rf/dispatch [:rems.administration.licenses/update-license %1 %2 [::enter-page id]])]
-      [status-flags/archived-toggle license #(rf/dispatch [:rems.administration.licenses/update-license %1 %2 [::enter-page id]])]])])
+                     [inline-info-field (text :t.administration/active) [readonly-checkbox (not (:expired license))]]]))}]
+   [:div.col.commands [back-button]]])
 
 ;; XXX: Duplicates much of license-view. One notable difference is that
 ;;      here the license text is only shown in the current language.
diff --git a/src/cljs/rems/administration/licenses.cljs b/src/cljs/rems/administration/licenses.cljs
index 129a9d24d..7119f3125 100644
--- a/src/cljs/rems/administration/licenses.cljs
+++ b/src/cljs/rems/administration/licenses.cljs
@@ -37,11 +37,11 @@
 
 (rf/reg-event-fx
  ::update-license
- (fn [_ [_ item description dispatch-on-finished]]
+ (fn [_ [_ item description]]
    (status-modal/common-pending-handler! description)
    (put! "/api/licenses/update"
          {:params (select-keys item [:id :enabled :archived])
-          :handler (partial status-flags/common-update-handler! #(rf/dispatch dispatch-on-finished))
+          :handler (partial status-flags/common-update-handler! #(rf/dispatch [::fetch-licenses]))
           :error-handler status-modal/common-error-handler!})
    {}))
 
@@ -79,14 +79,14 @@
            :end (let [value (:end license)]
                   {:value value
                    :display-value (localize-time value)})
-           :active (let [checked? (status-flags/active? license)]
+           :active (let [checked? (not (:expired license))]
                      {:td [:td.active
                            [readonly-checkbox checked?]]
                       :sort-value (if checked? 1 2)})
            :commands {:td [:td.commands
                            [to-view-license (:id license)]
-                           [status-flags/enabled-toggle license #(rf/dispatch [::update-license %1 %2 [::fetch-licenses]])]
-                           [status-flags/archived-toggle license #(rf/dispatch [::update-license %1 %2 [::fetch-licenses]])]]}})
+                           [status-flags/enabled-toggle license #(rf/dispatch [::update-license %1 %2])]
+                           [status-flags/archived-toggle license #(rf/dispatch [::update-license %1 %2])]]}})
         licenses)))
 
 (defn- licenses-list []
diff --git a/src/cljs/rems/administration/resource.cljs b/src/cljs/rems/administration/resource.cljs
index 19f716cf1..60fef7e9a 100644
--- a/src/cljs/rems/administration/resource.cljs
+++ b/src/cljs/rems/administration/resource.cljs
@@ -4,7 +4,6 @@
             [rems.administration.administration :refer [administration-navigator-container]]
             [rems.administration.components :refer [inline-info-field]]
             [rems.administration.license :refer [licenses-view]]
-            [rems.administration.status-flags :as status-flags]
             [rems.atoms :as atoms :refer [attachment-link external-link readonly-checkbox document-title]]
             [rems.collapsible :as collapsible]
             [rems.common-util :refer [andstr]]
@@ -49,13 +48,9 @@
               [inline-info-field (text :t.administration/resource) (:resid resource)]
               [inline-info-field (text :t.administration/start) (localize-time (:start resource))]
               [inline-info-field (text :t.administration/end) (localize-time (:end resource))]
-              [inline-info-field (text :t.administration/active) [readonly-checkbox (status-flags/active? resource)]]]}]
+              [inline-info-field (text :t.administration/active) [readonly-checkbox (not (:expired resource))]]]}]
    [licenses-view (:licenses resource) language]
-   (let [id (:id resource)]
-     [:div.col.commands
-      [back-button]
-      [status-flags/enabled-toggle resource #(rf/dispatch [:rems.administration.resources/update-resource %1 %2 [::enter-page id]])]
-      [status-flags/archived-toggle resource #(rf/dispatch [:rems.administration.resources/update-resource %1 %2 [::enter-page id]])]])])
+   [:div.col.commands [back-button]]])
 
 (defn resource-page []
   (let [resource (rf/subscribe [::resource])
diff --git a/src/cljs/rems/administration/resources.cljs b/src/cljs/rems/administration/resources.cljs
index fbe4d761e..fad3b02de 100644
--- a/src/cljs/rems/administration/resources.cljs
+++ b/src/cljs/rems/administration/resources.cljs
@@ -38,11 +38,11 @@
 
 (rf/reg-event-fx
  ::update-resource
- (fn [{:keys [db]} [_ item description dispatch-on-finished]]
+ (fn [{:keys [db]} [_ item description]]
    (status-modal/common-pending-handler! description)
    (put! "/api/resources/update"
          {:params (select-keys item [:id :enabled :archived])
-          :handler (partial status-flags/common-update-handler! #(rf/dispatch dispatch-on-finished))
+          :handler (partial status-flags/common-update-handler! #(rf/dispatch [::fetch-resources]))
           :error-handler status-modal/common-error-handler!})
    {}))
 
@@ -78,14 +78,14 @@
            :end (let [value (:end resource)]
                   {:value value
                    :display-value (localize-time value)})
-           :active (let [checked? (status-flags/active? resource)]
+           :active (let [checked? (not (:expired resource))]
                      {:td [:td.active
                            [readonly-checkbox checked?]]
                       :sort-value (if checked? 1 2)})
            :commands {:td [:td.commands
                            [to-view-resource (:id resource)]
-                           [status-flags/enabled-toggle resource #(rf/dispatch [::update-resource %1 %2 [::fetch-resources]])]
-                           [status-flags/archived-toggle resource #(rf/dispatch [::update-resource %1 %2 [::fetch-resources]])]]}})
+                           [status-flags/enabled-toggle resource #(rf/dispatch [::update-resource %1 %2])]
+                           [status-flags/archived-toggle resource #(rf/dispatch [::update-resource %1 %2])]]}})
         resources)))
 
 (defn- resources-list []
diff --git a/src/cljs/rems/administration/status_flags.cljs b/src/cljs/rems/administration/status_flags.cljs
index aaa8fab00..6dd1b63cd 100644
--- a/src/cljs/rems/administration/status_flags.cljs
+++ b/src/cljs/rems/administration/status_flags.cljs
@@ -38,6 +38,7 @@
     [unarchive-button item on-change]
     [archive-button item on-change]))
 
+
 (defn display-old-toggle [display-old? on-change]
   [:div.form-check.form-check-inline {:style {:float "right"}}
    [:input.form-check-input {:type "checkbox"
@@ -47,9 +48,6 @@
    [:label.form-check-label {:for "display-old"}
     (text :t.administration/display-old)]])
 
-(defn active? [item]
-  (and (not (:expired item)) (:enabled item) (not (:archived item))))
-
 (defn- format-update-error [{:keys [type catalogue-items forms licenses resources workflows]}]
   (let [language @(rf/subscribe [:language])]
     [:<>
diff --git a/src/cljs/rems/administration/workflow.cljs b/src/cljs/rems/administration/workflow.cljs
index ec6db7dfd..3f363278d 100644
--- a/src/cljs/rems/administration/workflow.cljs
+++ b/src/cljs/rems/administration/workflow.cljs
@@ -4,7 +4,6 @@
             [rems.administration.administration :refer [administration-navigator-container]]
             [rems.administration.components :refer [inline-info-field]]
             [rems.administration.license :refer [licenses-view]]
-            [rems.administration.status-flags :as status-flags]
             [rems.atoms :as atoms :refer [attachment-link external-link info-field readonly-checkbox enrich-user document-title]]
             [rems.collapsible :as collapsible]
             [rems.common-util :refer [andstr]]
@@ -63,14 +62,9 @@
                                                                          (str/join ", "))]
               [inline-info-field (text :t.administration/start) (localize-time (:start workflow))]
               [inline-info-field (text :t.administration/end) (localize-time (:end workflow))]
-              [inline-info-field (text :t.administration/active) [readonly-checkbox (status-flags/active? workflow)]]]}]
+              [inline-info-field (text :t.administration/active) [readonly-checkbox (not (:expired workflow))]]]}]
    [licenses-view (:licenses workflow) language]
-   (let [id (:id workflow)]
-     [:div.col.commands
-      [back-button]
-      [edit-button id]
-      [status-flags/enabled-toggle workflow #(rf/dispatch [:rems.administration.workflows/update-workflow %1 %2 [::enter-page id]])]
-      [status-flags/archived-toggle workflow #(rf/dispatch [:rems.administration.workflows/update-workflow %1 %2 [::enter-page id]])]])])
+   [:div.col.commands [back-button] [edit-button (:id workflow)]]])
 
 (defn workflow-page []
   (let [workflow (rf/subscribe [::workflow])
diff --git a/src/cljs/rems/administration/workflows.cljs b/src/cljs/rems/administration/workflows.cljs
index 2915525c2..0291da32e 100644
--- a/src/cljs/rems/administration/workflows.cljs
+++ b/src/cljs/rems/administration/workflows.cljs
@@ -37,11 +37,11 @@
 
 (rf/reg-event-fx
  ::update-workflow
- (fn [_ [_ item description dispatch-on-finished]]
+ (fn [_ [_ item description]]
    (status-modal/common-pending-handler! description)
    (put! "/api/workflows/update"
          {:params (select-keys item [:id :enabled :archived])
-          :handler (partial status-flags/common-update-handler! #(rf/dispatch dispatch-on-finished))
+          :handler (partial status-flags/common-update-handler! #(rf/dispatch [::fetch-workflows]))
           :error-handler status-modal/common-error-handler!})
    {}))
 
@@ -82,15 +82,15 @@
            :end (let [value (:end workflow)]
                   {:value value
                    :display-value (localize-time value)})
-           :active (let [checked? (status-flags/active? workflow)]
+           :active (let [checked? (not (:expired workflow))]
                      {:td [:td.active
                            [readonly-checkbox checked?]]
                       :sort-value (if checked? 1 2)})
            :commands {:td [:td.commands
                            [to-view-workflow (:id workflow)]
                            [to-edit-workflow (:id workflow)]
-                           [status-flags/enabled-toggle workflow #(rf/dispatch [::update-workflow %1 %2 [::fetch-workflows]])]
-                           [status-flags/archived-toggle workflow #(rf/dispatch [::update-workflow %1 %2 [::fetch-workflows]])]]}})
+                           [status-flags/enabled-toggle workflow #(rf/dispatch [::update-workflow %1 %2])]
+                           [status-flags/archived-toggle workflow #(rf/dispatch [::update-workflow %1 %2])]]}})
         workflows)))
 
 (defn- workflows-list []
diff --git a/src/cljs/rems/application.cljs b/src/cljs/rems/application.cljs
index c79ba841b..00e65d366 100644
--- a/src/cljs/rems/application.cljs
+++ b/src/cljs/rems/application.cljs
@@ -18,14 +18,14 @@
             [rems.actions.return-action :refer [return-action-button return-form]]
             [rems.actions.review :refer [review-action-button review-form]]
             [rems.application-util :refer [accepted-licenses? form-fields-editable? get-member-name]]
-            [rems.atoms :refer [external-link file-download flash-message info-field readonly-checkbox textarea document-title success-symbol]]
+            [rems.atoms :refer [external-link file-download info-field readonly-checkbox textarea document-title success-symbol]]
             [rems.collapsible :as collapsible]
             [rems.common-util :refer [index-by]]
             [rems.fields :as fields]
+            [rems.flash-message :as flash-message]
             [rems.guide-utils :refer [lipsum lipsum-short lipsum-paragraphs]]
             [rems.phase :refer [phases]]
             [rems.spinner :as spinner]
-            [rems.status-modal :as status-modal]
             [rems.text :refer [localize-decision localize-event localized localize-state localize-time text text-format]]
             [rems.util :refer [dispatch! fetch parse-int post! in-page-anchor-link]])
   (:require-macros [rems.guide-macros :refer [component-info example]]))
@@ -48,7 +48,7 @@
                                             (:catalogue-item/expired %)
                                             (:catalogue-item/archived %)))
                                seq)]
-      [:div.alert.alert-danger
+      [:div.alert.alert-warning
        (text :t.form/alert-disabled-resources)
        (into [:ul]
              (for [resource resources]
@@ -88,13 +88,14 @@
  ::enter-application-page
  (fn [{:keys [db]} [_ id]]
    {:db (dissoc db ::application ::edit-application)
-    ::fetch-application id}))
+    :dispatch [::fetch-application id]}))
 
-(rf/reg-fx
+(rf/reg-event-fx
  ::fetch-application
- (fn [id]
+ (fn [_ [_ id]]
    (fetch (str "/api/applications/" id)
-          {:handler #(rf/dispatch [::fetch-application-result %])})))
+          {:handler #(rf/dispatch [::fetch-application-result %])})
+   {}))
 
 (rf/reg-event-db
  ::fetch-application-result
@@ -134,12 +135,14 @@
     {:field field :value value}))
 
 (defn- save-application! [description application-id field-values]
-  (status-modal/common-pending-handler! description)
   (post! "/api/applications/save-draft"
          {:params {:application-id application-id
                    :field-values (field-values-to-api field-values)}
-          :handler (partial status-modal/common-success-handler! #(rf/dispatch [::enter-application-page application-id]))
-          :error-handler status-modal/common-error-handler!}))
+          :handler (flash-message/default-success-handler
+                    description
+                    (fn [_]
+                      (rf/dispatch [::fetch-application application-id])))
+          :error-handler (flash-message/default-error-handler description)}))
 
 (rf/reg-event-fx
  ::save-application
@@ -154,26 +157,30 @@
 (defn- submit-application! [application description application-id field-values
                             userid]
   ;; TODO: deduplicate with save-application!
-  (status-modal/common-pending-handler! description)
   (post! "/api/applications/save-draft"
          {:params {:application-id application-id
                    :field-values (field-values-to-api field-values)}
           :handler (fn [response]
-                     (if (:success response)
-                       (if (accepted-licenses? application userid)
-                         (post! "/api/applications/submit"
-                                {:params {:application-id application-id}
-                                 :handler (fn [response]
-                                            (if (:success response)
-                                              (status-modal/set-success! {:on-close #(rf/dispatch [::enter-application-page application-id])})
-                                              (do
-                                                (status-modal/set-error! {:result response
-                                                                          :error-content (format-validation-errors application (:errors response))})
-                                                (rf/dispatch [::set-validation-errors (:errors response)]))))
-                                 :error-handler status-modal/common-error-handler!})
-                         (status-modal/set-error! {:error-content (text :t.actions/licenses-not-accepted-error)}))
-                       (status-modal/common-error-handler! response)))
-          :error-handler status-modal/common-error-handler!}))
+                     (cond
+                       (not (:success response))
+                       (flash-message/show-default-error! description)
+
+                       (not (accepted-licenses? application userid))
+                       (flash-message/show-error! (text :t.actions/licenses-not-accepted-error))
+
+                       :else
+                       (post! "/api/applications/submit"
+                              {:params {:application-id application-id}
+                               :handler (fn [response]
+                                          (if (:success response)
+                                            (do
+                                              (rf/dispatch [::fetch-application application-id])
+                                              (flash-message/show-default-success! description))
+                                            (do
+                                              (rf/dispatch [::set-validation-errors (:errors response)])
+                                              (flash-message/show-error! [format-validation-errors application (:errors response)]))))
+                               :error-handler (flash-message/default-error-handler description)})))
+          :error-handler (flash-message/default-error-handler description)}))
 
 (rf/reg-event-fx
  ::submit-application
@@ -190,19 +197,20 @@
 (rf/reg-event-fx
  ::copy-as-new-application
  (fn [{:keys [db]} _]
-   (let [application-id (get-in db [::application :application/id])]
-     (status-modal/common-pending-handler! (text :t.form/copy-as-new))
+   (let [application-id (get-in db [::application :application/id])
+         description (text :t.form/copy-as-new)]
      (post! "/api/applications/copy-as-new"
             {:params {:application-id application-id}
-             :handler (partial status-modal/common-success-handler!
-                               (fn [response]
-                                 (dispatch! (str "/#/application/" (:application-id response)))))
-             :error-handler status-modal/common-error-handler!}))
+             :handler (flash-message/default-success-handler
+                       description
+                       (fn [response]
+                         (rf/dispatch [:rems.spa/user-triggered-navigation])
+                         (dispatch! (str "/#/application/" (:application-id response)))))
+             :error-handler (flash-message/default-error-handler description)}))
    {}))
 
 (defn- save-attachment [{:keys [db]} [_ field-id file description]]
   (let [application-id (get-in db [::application :application/id])]
-    (status-modal/common-pending-handler! description)
     (post! "/api/applications/add-attachment"
            {:url-params {:application-id application-id
                          :field-id field-id}
@@ -210,14 +218,13 @@
             ;; force saving a draft when you upload an attachment.
             ;; this ensures that the attachment is not left
             ;; dangling (with no references to it)
-            :handler (fn [response]
-                       (if (:success response)
-                         (do
-                           ;; no race condition here: events are handled in a FIFO manner
-                           (rf/dispatch [::set-field-value field-id (str (:id response))])
-                           (rf/dispatch [::save-application (text :t.form/upload)]))
-                         (status-modal/common-error-handler! response)))
-            :error-handler status-modal/common-error-handler!})
+            :handler (flash-message/default-success-handler
+                      description
+                      (fn [response]
+                        ;; no race condition here: events are handled in a FIFO manner
+                        (rf/dispatch [::set-field-value field-id (str (:id response))])
+                        (rf/dispatch [::save-application (text :t.form/upload)])))
+            :error-handler (flash-message/default-error-handler description)})
     {}))
 
 (rf/reg-event-fx ::save-attachment save-attachment)
@@ -637,23 +644,18 @@
                [:div#resource-action-forms
                 [change-resources-form application can-comment? (partial reload! application-id)]]]}]))
 
-(defn- render-application [application edit-application config userid]
-  (let [messages (remove nil?
-                         [(disabled-items-warning application) ; NB: eval this here so we get nil or a warning
-                          (when-let [errors (:validation-errors edit-application)]
-                            [flash-message
-                             {:status :danger
-                              :contents [format-validation-errors application errors]}])])]
-    [:div
-     [document-title (str (text :t.applications/application) " " (format-application-id config application))]
-     (text :t.applications/intro)
-     (into [:div] messages)
-     [application-state application config]
-     [:div.mt-3 [applicants-info application]]
-     [:div.mt-3 [applied-resources application userid]]
-     [:div.my-3 [application-licenses application edit-application userid]]
-     [:div.my-3 [application-fields application edit-application]]
-     [:div.mb-3 [actions-form application]]]))
+(defn- render-application [{:keys [application edit-application config userid]}]
+  [:div
+   [document-title (str (text :t.applications/application) " " (format-application-id config application))]
+   [flash-message/component]
+   [disabled-items-warning application]
+   (text :t.applications/intro)
+   [application-state application config]
+   [:div.mt-3 [applicants-info application]]
+   [:div.mt-3 [applied-resources application userid]]
+   [:div.my-3 [application-licenses application edit-application userid]]
+   [:div.my-3 [application-fields application edit-application]]
+   [:div.mb-3 [actions-form application]]])
 
 ;;;; Entrypoint
 
@@ -667,7 +669,10 @@
       [:div
        [document-title (text :t.applications/application)]
        [spinner/big]]
-      [render-application application edit-application config userid])))
+      [render-application {:application application
+                           :edit-application edit-application
+                           :config config
+                           :userid userid}])))
 
 
 ;;;; Guide
@@ -768,73 +773,73 @@
    (component-info render-application)
    (example "application, partially filled"
             [render-application
-             {:application/id 17
-              :application/state :application.state/draft
-              :application/resources [{:catalogue-item/title {:en "An applied item"}}]
-              :application/form {:form/fields [{:field/id 1
-                                                :field/type :text
-                                                :field/title {:en "Field 1"}
-                                                :field/placeholder {:en "placeholder 1"}}
-                                               {:field/id 2
-                                                :field/type :label
-                                                :title "Please input your wishes below."}
-                                               {:field/id 3
-                                                :field/type :texta
-                                                :field/optional true
-                                                :field/title {:en "Field 2"}
-                                                :field/placeholder {:en "placeholder 2"}}
-                                               {:field/id 4
-                                                :field/type :unsupported
-                                                :field/title {:en "Field 3"}
-                                                :field/placeholder {:en "placeholder 3"}}
-                                               {:field/id 5
-                                                :field/type :date
-                                                :field/title {:en "Field 4"}}]}
-              :application/licenses [{:license/id 4
-                                      :license/type :text
-                                      :license/title {:en "A Text License"}
-                                      :license/text {:en lipsum}}
-                                     {:license/id 5
-                                      :license/type :link
-                                      :license/title {:en "Link to license"}
-                                      :license/link {:en "https://creativecommons.org/licenses/by/4.0/deed.en"}}]}
-             {:field-values {1 "abc"}
-              :show-diff {}
-              :validation-errors nil
-              :accepted-licenses #{5}}])
+             {:application {:application/id 17
+                            :application/state :application.state/draft
+                            :application/resources [{:catalogue-item/title {:en "An applied item"}}]
+                            :application/form {:form/fields [{:field/id 1
+                                                              :field/type :text
+                                                              :field/title {:en "Field 1"}
+                                                              :field/placeholder {:en "placeholder 1"}}
+                                                             {:field/id 2
+                                                              :field/type :label
+                                                              :title "Please input your wishes below."}
+                                                             {:field/id 3
+                                                              :field/type :texta
+                                                              :field/optional true
+                                                              :field/title {:en "Field 2"}
+                                                              :field/placeholder {:en "placeholder 2"}}
+                                                             {:field/id 4
+                                                              :field/type :unsupported
+                                                              :field/title {:en "Field 3"}
+                                                              :field/placeholder {:en "placeholder 3"}}
+                                                             {:field/id 5
+                                                              :field/type :date
+                                                              :field/title {:en "Field 4"}}]}
+                            :application/licenses [{:license/id 4
+                                                    :license/type :text
+                                                    :license/title {:en "A Text License"}
+                                                    :license/text {:en lipsum}}
+                                                   {:license/id 5
+                                                    :license/type :link
+                                                    :license/title {:en "Link to license"}
+                                                    :license/link {:en "https://creativecommons.org/licenses/by/4.0/deed.en"}}]}
+              :edit-application {:field-values {1 "abc"}
+                                 :show-diff {}
+                                 :validation-errors nil
+                                 :accepted-licenses #{5}}}])
    (example "application, applied"
             [render-application
-             {:application/id 17
-              :application/state :application.state/submitted
-              :application/resources [{:catalogue-item/title {:en "An applied item"}}]
-              :application/form {:form/fields [{:field/id 1
-                                                :field/type :text
-                                                :field/title {:en "Field 1"}
-                                                :field/placeholder {:en "placeholder 1"}}]}
-              :application/licenses [{:license/id 4
-                                      :license/type :text
-                                      :license/title {:en "A Text License"}
-                                      :license/text {:en lipsum}}]}
-             {:field-values {1 "abc"}
-              :accepted-licenses #{4}}])
+             {:application {:application/id 17
+                            :application/state :application.state/submitted
+                            :application/resources [{:catalogue-item/title {:en "An applied item"}}]
+                            :application/form {:form/fields [{:field/id 1
+                                                              :field/type :text
+                                                              :field/title {:en "Field 1"}
+                                                              :field/placeholder {:en "placeholder 1"}}]}
+                            :application/licenses [{:license/id 4
+                                                    :license/type :text
+                                                    :license/title {:en "A Text License"}
+                                                    :license/text {:en lipsum}}]}
+              :edit-application {:field-values {1 "abc"}
+                                 :accepted-licenses #{4}}}])
    (example "application, approved"
             [render-application
-             {:application/id 17
-              :application/state :application.state/approved
-              :application/applicant-attributes {:eppn "eppn"
-                                                 :mail "email@example.com"
-                                                 :additional "additional field"}
-              :application/resources [{:catalogue-item/title {:en "An applied item"}}]
-              :application/form {:form/fields [{:field/id 1
-                                                :field/type :text
-                                                :field/title {:en "Field 1"}
-                                                :field/placeholder {:en "placeholder 1"}}]}
-              :application/licenses [{:license/id 4
-                                      :license/type :text
-                                      :license/title {:en "A Text License"}
-                                      :license/text {:en lipsum}}]}
-             {:field-values {1 "abc"}
-              :accepted-licenses #{4}}])
+             {:application {:application/id 17
+                            :application/state :application.state/approved
+                            :application/applicant-attributes {:eppn "eppn"
+                                                               :mail "email@example.com"
+                                                               :additional "additional field"}
+                            :application/resources [{:catalogue-item/title {:en "An applied item"}}]
+                            :application/form {:form/fields [{:field/id 1
+                                                              :field/type :text
+                                                              :field/title {:en "Field 1"}
+                                                              :field/placeholder {:en "placeholder 1"}}]}
+                            :application/licenses [{:license/id 4
+                                                    :license/type :text
+                                                    :license/title {:en "A Text License"}
+                                                    :license/text {:en lipsum}}]}
+              :edit-application {:field-values {1 "abc"}
+                                 :accepted-licenses #{4}}}])
 
    (component-info application-copy-notice)
    (example "no copies"
diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs
index 453dc98a9..936a47144 100644
--- a/src/cljs/rems/atoms.cljs
+++ b/src/cljs/rems/atoms.cljs
@@ -52,7 +52,7 @@
    :contents - content to show inside the notification"
   [{status :status contents :contents}]
   (when status
-    [:div.alert {:class (str "alert-" (name status))} contents]))
+    [:div#flash-message.alert {:class (str "alert-" (name status))} contents]))
 
 (defn readonly-checkbox
   "Displays a checkbox."
diff --git a/src/cljs/rems/flash_message.cljs b/src/cljs/rems/flash_message.cljs
new file mode 100644
index 000000000..a0e0d325f
--- /dev/null
+++ b/src/cljs/rems/flash_message.cljs
@@ -0,0 +1,72 @@
+(ns rems.flash-message
+  (:require [clojure.string :as str]
+            [re-frame.core :as rf]
+            [reagent.core :as reagent]
+            [rems.atoms :as atoms]
+            [rems.focus :as focus]
+            [rems.text :refer [text]]))
+
+(rf/reg-sub ::message (fn [db _] (::message db)))
+
+(rf/reg-event-fx
+ ::reset
+ (fn [{:keys [db]} _]
+   {:db (dissoc db ::message)}))
+
+(rf/reg-event-fx
+ ::show-flash-message
+ (fn [{:keys [db]} [_ message]]
+   ;; On Chrome, focusing the element scrolls it fully into view,
+   ;; but on Firefox the element is hidden behind the navigation menu,
+   ;; so explicit scrolling is needed.
+   (.scrollTo js/window 0 0)
+   (focus/focus-element-async "#flash-message")
+   ;; TODO: flash the message with CSS
+   {:db (assoc db ::message message)}))
+
+(defn show-success! [contents]
+  (rf/dispatch [::show-flash-message {:status :success
+                                      :contents contents
+                                      :page @(rf/subscribe [:page])}]))
+
+(defn show-error! [contents]
+  (rf/dispatch [::show-flash-message {:status :danger
+                                      :contents contents
+                                      :page @(rf/subscribe [:page])}]))
+
+(defn component []
+  (reagent/create-class
+   {:display-name "rems.flash-message/component"
+
+    :component-will-unmount
+    (fn [_this]
+      (rf/dispatch [::reset]))
+
+    :reagent-render
+    (fn []
+      (let [message @(rf/subscribe [::message])]
+        [atoms/flash-message message]))}))
+
+;;; Helpers for typical messages
+
+(defn show-default-success! [description]
+  (show-success! [:span#status-success
+                  (str description ": " (text :t.form/success) ".")]))
+
+(defn show-default-error! [description & more]
+  (show-error! [:span#status-failed
+                (str/join " " (concat [(str description ": " (text :t.form/failed) ".")]
+                                      more))]))
+
+(defn default-success-handler [description on-success]
+  (fn [response]
+    (if (:success response)
+      (do
+        (show-default-success! description)
+        (when on-success
+          (on-success response)))
+      (show-default-error! description))))
+
+(defn default-error-handler [description]
+  (fn [response]
+    (show-default-error! description (:status-text response))))
diff --git a/src/cljs/rems/focus.cljs b/src/cljs/rems/focus.cljs
index 002b8ae13..18f41988f 100644
--- a/src/cljs/rems/focus.cljs
+++ b/src/cljs/rems/focus.cljs
@@ -1,30 +1,14 @@
 (ns rems.focus
-  "Focuses an HTML element as soon as it exists.
-
-  The component using this needs to acquire a reference to the element to be
-  focused using React's refs, and store it in an atom as instructed in
-  Reagent's documentation. [1] But instead of setting the atom directly, set it
-  using the `ref-changed` function.
-
-  When the component needs to focus an element, call `set-focus` with the atom
-  which contains or will contain the element to be focused. The element will
-  be focused as soon as possible, regardless of the order in which `set-focus`
-  and `ref-changed` are called.
-
-  [1] https://cljdoc.org/d/reagent/reagent/0.8.1/doc/frequently-asked-questions/how-do-i-use-react-s-refs-")
-
-(def ^:private element-atom-to-be-focused (atom nil))
-
-(defn- check-if-need-to-focus []
-  (when-let [element-atom @element-atom-to-be-focused]
-    (when-let [element @element-atom]
-      (.focus element)
-      (reset! element-atom-to-be-focused nil))))
-
-(defn set-focus [element-atom]
-  (reset! element-atom-to-be-focused element-atom)
-  (check-if-need-to-focus))
-
-(defn ref-changed [element-atom element]
-  (reset! element-atom element)
-  (check-if-need-to-focus))
+  "Focuses an HTML element as soon as it exists.")
+
+(defn focus-element-async
+  ([selector]
+   (focus-element-async selector 100))
+  ([selector tries]
+   (when (pos? tries)
+     (if-let [element (.querySelector js/document selector)]
+       (do
+         (.setAttribute element "tabindex" "-1")
+         (.focus element))
+       (js/setTimeout #(focus-element-async selector (dec tries))
+                      10)))))
diff --git a/src/cljs/rems/spa.cljs b/src/cljs/rems/spa.cljs
index 8b7ce9054..ae36358b4 100644
--- a/src/cljs/rems/spa.cljs
+++ b/src/cljs/rems/spa.cljs
@@ -345,10 +345,6 @@
   (rf/dispatch [:rems.administration.create-catalogue-item/enter-page])
   (rf/dispatch [:set-active-page :rems.administration/create-catalogue-item]))
 
-(secretary/defroute "/administration/edit-catalogue-item/:catalogue-item-id" [catalogue-item-id]
-  (rf/dispatch [:rems.administration.create-catalogue-item/enter-page (parse-int catalogue-item-id)])
-  (rf/dispatch [:set-active-page :rems.administration/create-catalogue-item]))
-
 (secretary/defroute "/administration/create-form" []
   (rf/dispatch [:rems.administration.create-form/enter-page])
   (rf/dispatch [:set-active-page :rems.administration/create-form]))
diff --git a/src/cljs/rems/table.cljs b/src/cljs/rems/table.cljs
index fd454d557..e734dadf6 100644
--- a/src/cljs/rems/table.cljs
+++ b/src/cljs/rems/table.cljs
@@ -1,7 +1,9 @@
 (ns rems.table
-  (:require [clojure.string :as str]
+  (:require [cljs.test :refer-macros [deftest is testing]]
+            [clojure.string :as str]
             [re-frame.core :as rf]
             [rems.atoms :refer [close-symbol search-symbol sort-symbol]]
+            [rems.focus :as focus]
             [rems.search :as search]
             [rems.text :refer [text-format]])
   (:require-macros [rems.guide-macros :refer [component-info example]]))
@@ -67,18 +69,14 @@
     m
     (assoc m k (make-default m))))
 
-(defn apply-row-defaults [row]
+(defn- apply-row-defaults [row]
   (->> row
        (map (fn [[column opts]]
               [column
                (if (= :key column) ; not a column, but the row key
                  opts
                  (-> opts
-                     (default-if-missing :sort-value (fn [opts]
-                                                       (let [val (:value opts)]
-                                                         (if (string? val)
-                                                           (str/lower-case val)
-                                                           val))))
+                     (default-if-missing :sort-value :value)
                      (default-if-missing :display-value (comp str :value))
                      (default-if-missing :filter-value (fn [opts]
                                                          (if (string? (:display-value opts))
@@ -89,6 +87,56 @@
                                                 (:display-value opts)]))))]))
        (into {})))
 
+(deftest test-apply-row-defaults
+  (testing "all custom"
+    (is (= {:key 123
+            :foo {:sort-value "foo1"
+                  :display-value "foo2"
+                  :filter-value "foo3"
+                  :td [:td "foo4"]}}
+           (apply-row-defaults {:key 123
+                                :foo {:sort-value "foo1"
+                                      :display-value "foo2"
+                                      :filter-value "foo3"
+                                      :td [:td "foo4"]}}))))
+  (testing "all defaults"
+    (is (= {:key 123
+            :foo {:value 42
+                  :sort-value 42
+                  :display-value "42"
+                  :filter-value "42"
+                  :td [:td {:class "foo"} "42"]}}
+           (apply-row-defaults {:key 123
+                                :foo {:value 42}}))))
+  (testing "component only"
+    (is (= {:key 123
+            :foo {:sort-value nil
+                  :display-value ""
+                  :filter-value ""
+                  :td [:td.foo [:button "Button"]]}}
+           (apply-row-defaults {:key 123
+                                :foo {:td [:td.foo [:button "Button"]]}}))))
+  (testing ":filter-value is normalized to lowercase"
+    (is (= {:key 123
+            :foo {:sort-value ""
+                  :display-value "FooBar"
+                  :filter-value "foobar"
+                  :td [:td ""]}}
+           (apply-row-defaults {:key 123
+                                :foo {:sort-value ""
+                                      :display-value "FooBar"
+                                      :td [:td ""]}}))))
+  (testing "cannot calculate :filter-value from non-string :display-value"
+    (is (= {:key 123
+            :foo {:sort-value ""
+                  :display-value [:p "foo"]
+                  :filter-value ""
+                  :td [:td ""]}}
+           (apply-row-defaults {:key 123
+                                :foo {:sort-value ""
+                                      :display-value [:p "foo"]
+                                      :td [:td ""]}})))))
+
 (rf/reg-sub
  ::rows
  (fn [[_ table] _]
@@ -127,6 +175,14 @@
   (->> (re-seq #"\S+" (str s))
        (map str/lower-case)))
 
+(deftest test-parse-search-terms
+  (is (= [] (parse-search-terms nil)))
+  (is (= [] (parse-search-terms "")))
+  (is (= ["word"] (parse-search-terms "word")))
+  (is (= ["uppercase"] (parse-search-terms "UPPERCASE")))
+  (is (= ["two" "words"] (parse-search-terms "two words")))
+  (is (= ["white" "space"] (parse-search-terms "   white \t\n space  "))))
+
 (rf/reg-sub
  ::sorted-and-filtered-rows
  (fn [[_ table] _]
@@ -171,18 +227,6 @@
         (for [column (:columns table)]
           (get-in row [(:key column) :td]))))
 
-(defn- focus-element-async
-  ([selector]
-   (focus-element-async selector 100))
-  ([selector tries]
-   (when (pos? tries)
-     (if-let [element (.querySelector js/document selector)]
-       (do
-         (.setAttribute element "tabindex" "-1")
-         (.focus element))
-       (js/setTimeout #(focus-element-async selector (dec tries))
-                      10)))))
-
 (defn table [table]
   (let [rows @(rf/subscribe [::sorted-and-filtered-rows table])
         language @(rf/subscribe [:language])
@@ -207,8 +251,8 @@
                                          :on-click (fn []
                                                      (rf/dispatch [::show-all-rows table])
                                                      (let [next-row (:key (nth rows max-rows))]
-                                                       (focus-element-async (str "table.rems-table." (name (:id table))
-                                                                                 " > tbody > tr[data-row='" next-row "'] > td"))))}
+                                                       (focus/focus-element-async (str "table.rems-table." (name (:id table))
+                                                                                       " > tbody > tr[data-row='" next-row "'] > td"))))}
                 (text-format :t.table/show-all-n-rows (count rows))]]]])]]))
 
 (defn guide []


Test patch:
diff --git a/src/clj/rems/db/test_data.clj b/src/clj/rems/db/test_data.clj
index f466f5028..1e02231a5 100644
--- a/src/clj/rems/db/test_data.clj
+++ b/src/clj/rems/db/test_data.clj
@@ -1,7 +1,6 @@
 (ns rems.db.test-data
   "Populating the database with nice test data."
   (:require [clj-time.core :as time]
-            [medley.core :refer [map-vals]]
             [rems.api.services.catalogue :as catalogue]
             [rems.api.services.licenses :as licenses]
             [rems.api.services.resource :as resource]
@@ -129,14 +128,18 @@
 
 (defn create-catalogue-item! [{:keys [title resource-id form-id workflow-id]
                                :as command}]
-  (let [localizations (map-vals (fn [title] {:title title}) title)
-        result (catalogue/create-catalogue-item!
-                {:resid (or resource-id (create-resource! {}))
-                 :form (or form-id (create-form! {}))
-                 :wfid (or workflow-id (create-dynamic-workflow! {}))
-                 :localizations (or localizations {})})]
-    (assert (:success result) {:command command :result result})
-    (:id result)))
+  (let [result (catalogue/create-catalogue-item! {:resid (or resource-id (create-resource! {}))
+                                                  :form (or form-id (create-form! {}))
+                                                  :wfid (or workflow-id (create-dynamic-workflow! {}))})
+        _ (assert (:success result) {:command command :result result})
+        id (:id result)]
+    (when (map? title)
+      (doseq [[lang text] title]
+        (let [result (catalogue/create-catalogue-item-localization! {:id id
+                                                                     :langcode (name lang)
+                                                                     :title text})]
+          (assert (:success result) {:command [id lang text] :result result}))))
+    id))
 
 (defn create-application! [{:keys [catalogue-item-ids actor]}]
   (:application-id (command! {:type :application.command/create
diff --git a/test/clj/rems/api/services/test_catalogue.clj b/test/clj/rems/api/services/test_catalogue.clj
index 502baec01..f2a34c197 100644
--- a/test/clj/rems/api/services/test_catalogue.clj
+++ b/test/clj/rems/api/services/test_catalogue.clj
@@ -127,22 +127,6 @@
       (archive-resource! false)
       (is (:success (archive-catalogue-item! true))))))
 
-(deftest test-edit-catalogue-item
-  (let [item-id (test-data/create-catalogue-item!
-                 {:title {:en "Old title"
-                          :fi "Vanha nimi"}})
-        old-item (first (catalogue/get-localized-catalogue-items))
-
-        _ (catalogue/edit-catalogue-item!
-           {:id item-id
-            :localizations {:en {:title "New title"}
-                            :fi {:title "Uusi nimi"}}})
-        new-item (first (catalogue/get-localized-catalogue-items))]
-    (is (= "Old title" (get-in old-item [:localizations :en :title])))
-    (is (= "Vanha nimi" (get-in old-item [:localizations :fi :title])))
-    (is (= "New title" (get-in new-item [:localizations :en :title])))
-    (is (= "Uusi nimi" (get-in new-item [:localizations :fi :title])))))
-
 (deftest test-get-localized-catalogue-items
   (let [item-id (test-data/create-catalogue-item! {})]
 
diff --git a/test/clj/rems/api/test_administration.clj b/test/clj/rems/api/test_administration.clj
index ab591c29d..73ceba02a 100644
--- a/test/clj/rems/api/test_administration.clj
+++ b/test/clj/rems/api/test_administration.clj
@@ -29,8 +29,7 @@
         catalogue-id (:id (api-call :post "/api/catalogue-items/create"
                                     {:form form-id
                                      :resid resource-id
-                                     :wfid workflow-id
-                                     :localizations {}}
+                                     :wfid workflow-id}
                                     api-key user-id))
 
         update-resource! (fn [{:keys [enabled archived]}]
diff --git a/test/clj/rems/api/test_catalogue_items.clj b/test/clj/rems/api/test_catalogue_items.clj
index 1f53a1e77..9e5717fc7 100644
--- a/test/clj/rems/api/test_catalogue_items.clj
+++ b/test/clj/rems/api/test_catalogue_items.clj
@@ -25,8 +25,7 @@
                    (json-body {:form form-id
                                :resid 1
                                :wfid 1
-                               :archived true
-                               :localizations {}})
+                               :archived true})
                    handler
                    read-body)
           id (:id data)]
@@ -39,9 +38,8 @@
         (is (= {:id id
                 :workflow-name "dynamic workflow"
                 :form-name "form name"
-                :resource-name "urn:nbn:fi:lb-201403262"
-                :localizations {}}
-               (select-keys data [:id :workflow-name :form-name :resource-name :localizations])))))
+                :resource-name "urn:nbn:fi:lb-201403262"}
+               (select-keys data [:id :workflow-name :form-name :resource-name])))))
     (testing "not found"
       (let [response (-> (request :get "/api/catalogue-items/777777777")
                          (authenticate api-key user-id)
@@ -74,23 +72,24 @@
                (assoc-in [:headers "x-rems-api-key"] "invalid-api-key")
                (json-body {:form 1
                            :resid 1
-                           :wfid 1
-                           :localizations {}})
+                           :wfid 1})
                handler
                (read-body)))))
-  (testing "edit without authentication"
-    (let [response (-> (request :post (str "/api/catalogue-items/edit"))
+  (testing "create-localization without authentication"
+    (let [response (-> (request :post (str "/api/catalogue-items/create-localization"))
                        (json-body {:id 1
-                                   :localizations {:en {:title "malicious localization"}}})
+                                   :langcode :fi
+                                   :title "malicious localization"})
                        handler)
           body (read-body response)]
       (is (response-is-unauthorized? response))
       (is (str/includes? body "Invalid anti-forgery token"))))
-  (testing "edit with wrong API-Key"
+  (testing "create-localization with wrong API-Key"
     (is (= "invalid api key"
-           (-> (request :post (str "/api/catalogue-items/edit"))
+           (-> (request :post (str "/api/catalogue-items/create-localization"))
                (assoc-in [:headers "x-rems-api-key"] "invalid-api-key")
                (json-body {:id 1
-                           :localizations {:en {:title "malicious localization"}}})
+                           :langcode :fi
+                           :title "malicious localization"})
                handler
                (read-body))))))
diff --git a/test/clj/rems/api/test_forms.clj b/test/clj/rems/api/test_forms.clj
index dc24d36db..649dd8327 100644
--- a/test/clj/rems/api/test_forms.clj
+++ b/test/clj/rems/api/test_forms.clj
@@ -158,8 +158,7 @@
                    (json-body {:form form-id
                                :resid 1
                                :wfid 1
-                               :archived false
-                               :localizations {}})
+                               :archived false})
                    handler
                    read-body)]
       (testing "Form is non-editable after in use by a catalogue item"
diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj
index 3ab9b11fb..d6f2ce69d 100644
--- a/test/clj/rems/test_browser.clj
+++ b/test/clj/rems/test_browser.clj
@@ -172,7 +172,6 @@
   (doto *driver*
     (scroll-and-click :submit)
     (wait-visible :status-success)
-    (scroll-and-click :modal-ok)
     (wait-has-class :apply-phase "completed")))
 
 (defn get-application-id []
diff --git a/test/cljs/rems/administration/test_create_catalogue_item.cljs b/test/cljs/rems/administration/test_create_catalogue_item.cljs
index b56b0f0d6..7bd1d2d22 100644
--- a/test/cljs/rems/administration/test_create_catalogue_item.cljs
+++ b/test/cljs/rems/administration/test_create_catalogue_item.cljs
@@ -5,17 +5,22 @@
 (deftest build-request-test
   (let [form {:title {:en "en title"
                       :fi "fi title"}
-              :workflow-id 123
-              :resource-id 456
-              :form-id 789}
+              :workflow {:id 123
+                         :unrelated "stuff"}
+              :resource {:id 456
+                         :unrelated "stuff"}
+              :form {:id 789
+                     :unrelated "stuff"}}
         languages [:en :fi]]
 
     (testing "valid form"
       (is (= {:wfid 123
               :resid 456
               :form 789
-              :localizations {:en {:title "en title"}
-                              :fi {:title "fi title"}}}
+              :localizations [{:langcode "en"
+                               :title "en title"}
+                              {:langcode "fi"
+                               :title "fi title"}]}
              (build-request form languages))))
 
     (testing "missing title"
@@ -23,12 +28,12 @@
                                languages)))
       (is (nil? (build-request (assoc-in form [:title :fi] "")
                                languages))))
-    (testing "missing workflow id"
-      (is (nil? (build-request (assoc form :workflow-id nil)
+    (testing "missing workflow"
+      (is (nil? (build-request (assoc form :workflow nil)
                                languages))))
-    (testing "missing resource id"
-      (is (nil? (build-request (assoc form :resource-id nil)
+    (testing "missing resource"
+      (is (nil? (build-request (assoc form :resource nil)
                                languages))))
-    (testing "missing form id"
-      (is (nil? (build-request (assoc form :form-id nil)
+    (testing "missing form"
+      (is (nil? (build-request (assoc form :form nil)
                                languages))))))
diff --git a/test/cljs/rems/cljs_tests.cljs b/test/cljs/rems/cljs_tests.cljs
index 3d97ee9d5..559bcfbcc 100644
--- a/test/cljs/rems/cljs_tests.cljs
+++ b/test/cljs/rems/cljs_tests.cljs
@@ -6,8 +6,8 @@
             rems.administration.test-create-resource
             rems.administration.test-create-workflow
             rems.administration.test-items
+            rems.table
             rems.test-fields
-            rems.test-table
             rems.test-util))
 
 (doo-tests 'rems.administration.test-create-catalogue-item
@@ -16,6 +16,6 @@
            'rems.administration.test-create-resource
            'rems.administration.test-create-workflow
            'rems.administration.test-items
+           'rems.table
            'rems.test-fields
-           'rems.test-table
            'rems.test-util)
diff --git a/test/cljs/rems/test_table.cljs b/test/cljs/rems/test_table.cljs
deleted file mode 100644
index 4ad1062dc..000000000
--- a/test/cljs/rems/test_table.cljs
+++ /dev/null
@@ -1,104 +0,0 @@
-(ns rems.test-table
-  (:require [cljs.test :refer-macros [deftest is testing]]
-            [rems.table :refer [apply-row-defaults parse-search-terms]]))
-
-(deftest test-apply-row-defaults
-  (testing "all custom"
-    (is (= {:key 123
-            :foo {:sort-value "foo1"
-                  :display-value "foo2"
-                  :filter-value "foo3"
-                  :td [:td "foo4"]}}
-           (apply-row-defaults {:key 123
-                                :foo {:sort-value "foo1"
-                                      :display-value "foo2"
-                                      :filter-value "foo3"
-                                      :td [:td "foo4"]}}))))
-  (testing "all defaults, string value"
-    (is (= {:key 123
-            :foo {:value "foo"
-                  :sort-value "foo"
-                  :display-value "foo"
-                  :filter-value "foo"
-                  :td [:td {:class "foo"} "foo"]}}
-           (apply-row-defaults {:key 123
-                                :foo {:value "foo"}}))))
-  (testing "all defaults, non-string value"
-    (is (= {:key 123
-            :foo {:value 42
-                  :sort-value 42
-                  :display-value "42"
-                  :filter-value "42"
-                  :td [:td {:class "foo"} "42"]}}
-           (apply-row-defaults {:key 123
-                                :foo {:value 42}}))))
-  (testing "component only"
-    (is (= {:key 123
-            :foo {:sort-value nil
-                  :display-value ""
-                  :filter-value ""
-                  :td [:td.foo [:button "Button"]]}}
-           (apply-row-defaults {:key 123
-                                :foo {:td [:td.foo [:button "Button"]]}}))))
-  (testing ":value is normalized to lowercase in :sort-value"
-    (is (= {:key 123
-            :foo {:value "FooBar"
-                  :sort-value "foobar"
-                  :display-value "FooBar"
-                  :filter-value ""
-                  :td [:td ""]}}
-           (apply-row-defaults {:key 123
-                                :foo {:value "FooBar"
-                                      :filter-value ""
-                                      :td [:td ""]}}))))
-  (testing ":sort-value retains non-lowercase string if explicitly set"
-    (is (= {:key 123
-            :foo {:value "FooBar"
-                  :sort-value "FooBar"
-                  :display-value "FooBar"
-                  :filter-value ""
-                  :td [:td ""]}}
-           (apply-row-defaults {:key 123
-                                :foo {:value "FooBar"
-                                      :sort-value "FooBar"
-                                      :filter-value ""
-                                      :td [:td ""]}}))))
-  (testing ":value is normalized to lowercase in :filter-value"
-    (is (= {:key 123
-            :foo {:sort-value ""
-                  :display-value "FooBar"
-                  :filter-value "foobar"
-                  :td [:td ""]}}
-           (apply-row-defaults {:key 123
-                                :foo {:sort-value ""
-                                      :display-value "FooBar"
-                                      :td [:td ""]}}))))
-  (testing ":filter-value retains non-lowercase string if explicitly set"
-    (is (= {:key 123
-            :foo {:sort-value ""
-                  :display-value "FooBar"
-                  :filter-value "FooBar"
-                  :td [:td ""]}}
-           (apply-row-defaults {:key 123
-                                :foo {:sort-value ""
-                                      :display-value "FooBar"
-                                      :filter-value "FooBar"
-                                      :td [:td ""]}}))))
-  (testing "cannot calculate :filter-value from non-string :display-value"
-    (is (= {:key 123
-            :foo {:sort-value ""
-                  :display-value [:p "foo"]
-                  :filter-value ""
-                  :td [:td ""]}}
-           (apply-row-defaults {:key 123
-                                :foo {:sort-value ""
-                                      :display-value [:p "foo"]
-                                      :td [:td ""]}})))))
-
-(deftest test-parse-search-terms
-  (is (= [] (parse-search-terms nil)))
-  (is (= [] (parse-search-terms "")))
-  (is (= ["word"] (parse-search-terms "word")))
-  (is (= ["uppercase"] (parse-search-terms "UPPERCASE")))
-  (is (= ["two" "words"] (parse-search-terms "two words")))
-  (is (= ["white" "space"] (parse-search-terms "   white \t\n space  "))))