Repository: CSCfi/rems
Pull request #3347 (2024-11-12 16:56:53)

Admin UI tunes
on top of #3345

More technical changes:
- `focus` no longer removes tabindex from element that has tabindex
- fixed (rare) issue with rate limited button where fast clicking between render could trigger JS-equivalent of null pointer exception

# Checklist for author

Remove items that aren't applicable, check items that are done.

## Reviewability
- [ ] Link to issue (no issue)
- [x] Note if PR is on top of other PR
- [ ] Consider adding screenshots for ease of review

## Backwards compatibility
- [ ] API is backwards compatible or completely new
- [ ] Events are backwards compatible _or_ have migrations
- [ ] Config is backwards compatible
- [ ] Feature appears correctly in PDF print

## Documentation
- [ ] Update changelog if necessary
- [ ] API is documented and shows up in Swagger UI
- [ ] Components are added to guide page
- [ ] Update docs/ (if applicable)
- [ ] Update manual/ (if applicable)
- [ ] ADR for major architectural decisions or experiments
- [ ] New config options in config-defaults.edn

## Testing
- [ ] Complex logic is unit tested
- [ ] Valuable features are integration / browser / acceptance tested automatically

## Follow-up
- [ ] New tasks are created for pending or remaining tasks



Patch:
diff --git a/.cljfmt.edn b/.cljfmt.edn
index b888f7ff7..2a35411fe 100644
--- a/.cljfmt.edn
+++ b/.cljfmt.edn
@@ -1,6 +1,3 @@
 {:paths ["project.clj" "src/clj" "src/cljc" "src/cljs" "test/clj" "test/cljc" "test/cljs"] ; need explicit paths to include cljs
- :extra-indents {delay [[:inner 0]]
-                 ;; macros, originally defined in rems.api.test-catalogue-items
-                 is-logged? [[:block 3]] ; :default indent for arguments [0 1 2], and :inner afterwards
-                 is-not-logged? [[:block 3]]}
+ :extra-indents {delay [[:inner 0]]}
  :remove-consecutive-blank-lines? false} ; too many changes for now, probably not desirable
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c32fb3dd..6d545fef1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,9 +8,6 @@ have notable changes.
 
 Changes since v2.37
 
-### Additions
-- Catalogue tree now supports keyboard interactions for ARIA role="tree". Navigation works with arrow keys and Home/End, tree nodes can be opened/closed with Enter key, and default action (like add to cart) works with Enter key. (#3336)
-
 ### Changes
 - The logging of the simultaneous request count (`rqc:`) is now done for the request and response
   log lines only, and separately. Previously the same value was used for all logging in a request,
diff --git a/src/clj/rems/application/model.clj b/src/clj/rems/application/model.clj
index cc6a227ac..df446db77 100644
--- a/src/clj/rems/application/model.clj
+++ b/src/clj/rems/application/model.clj
@@ -470,48 +470,34 @@
 (defn enrich-forms [forms get-form-template]
   (mapv #(enrich-form % get-form-template) forms))
 
-(defn- enrich-application-forms [application get-form-template]
-  (update application
-          :application/forms
-          #(enrich-forms % get-form-template)))
-
-(defn- enrich-application-description [application]
+(defn- set-application-description [application]
   (let [fields (select [:application/forms ALL :form/fields ALL] application)
         description (->> fields
                          (find-first #(= :description (:field/type %)))
                          :field/value)]
     (assoc application :application/description (str description))))
 
-(defn- enrich-resources [resources get-catalogue-item get-resource get-config]
+(defn- enrich-resources [resources get-catalogue-item get-resource]
   (->> resources
        (map (fn [resource]
               (let [item (get-catalogue-item (:catalogue-item/id resource))
-                    enriched-resource {:catalogue-item/id (:catalogue-item/id resource)
-                                       :resource/ext-id (:resource/ext-id resource)
-                                       :resource/id (:resource-id item)
-                                       :catalogue-item/title (localization-for :title item)
-                                       :catalogue-item/infourl (localization-for :infourl item)
-                                       ;; TODO: remove unused keys
-                                       :catalogue-item/start (:start item)
-                                       :catalogue-item/end (:end item)
-                                       :catalogue-item/enabled (:enabled item)
-                                       :catalogue-item/expired (:expired item)
-                                       :catalogue-item/archived (:archived item)}]
-
-                (if (:enable-duo (get-config))
-                  (let [resource-id (:resource-id item) ; this is a bit backwards approach
-                        duo-codes (-> (get-resource resource-id) :resource/duo :duo/codes)]
-                    (assoc-some-in enriched-resource [:resource/duo :duo/codes] (seq (duo/enrich-duo-codes duo-codes))))
-
-                  enriched-resource))))
+                    resource-id (:resource-id item) ; this is a bit backwards approach
+                    duo-codes (-> (get-resource resource-id) :resource/duo :duo/codes)]
+                (-> {:catalogue-item/id (:catalogue-item/id resource)
+                     :resource/ext-id (:resource/ext-id resource)
+                     :resource/id (:resource-id item)
+                     :catalogue-item/title (localization-for :title item)
+                     :catalogue-item/infourl (localization-for :infourl item)
+                     ;; TODO: remove unused keys
+                     :catalogue-item/start (:start item)
+                     :catalogue-item/end (:end item)
+                     :catalogue-item/enabled (:enabled item)
+                     :catalogue-item/expired (:expired item)
+                     :catalogue-item/archived (:archived item)}
+                    (assoc-some-in [:resource/duo :duo/codes] (seq (duo/enrich-duo-codes duo-codes)))))))
        (sort-by :catalogue-item/id)
        vec))
 
-(defn- enrich-application-resources [application get-catalogue-item get-resource get-config]
-  (update application
-          :application/resources
-          #(enrich-resources % get-catalogue-item get-resource get-config)))
-
 (defn- validate-duo-match [dataset-code query-code resource]
   (let [validity (duo/check-duo-code dataset-code query-code)]
     {:validity validity
@@ -538,16 +524,18 @@
      :resource/id (:resource/id resource)
      :duo/validation (validate-duo-match dataset-code query-code resource)}))
 
-(defn- enrich-duos [application get-config]
+(defn- hide-duos-if-not-enabled [application get-config]
   (if (:enable-duo (get-config))
+    application
     (-> application
-        (update-existing-in [:application/duo :duo/codes] duo/enrich-duo-codes)
-        (update-existing :application/duo assoc-some :duo/matches (seq (get-duo-matches application))))
-
-    (-> application ; hide instead
         (dissoc :application/duo)
         (update :application/resources (partial mapv #(dissoc % :resource/duo))))))
 
+(defn- enrich-duos [application]
+  (-> application
+      (update-existing-in [:application/duo :duo/codes] duo/enrich-duo-codes)
+      (update-existing :application/duo assoc-some :duo/matches (seq (get-duo-matches application)))))
+
 (defn- enrich-workflow-licenses [application get-workflow]
   (let [wf (get-workflow (get-in application [:application/workflow :workflow/id]))]
     (if-some [workflow-licenses (seq (get-in wf [:workflow :licenses]))]
@@ -579,11 +567,6 @@
                            (sort-by :license/id))]
     (merge-lists-by :license/id rich-licenses app-licenses)))
 
-(defn- enrich-application-licenses [application get-license]
-  (update application
-          :application/licenses
-          #(enrich-licenses % get-license)))
-
 (def ^:private sensitive-events #{:application.event/review-requested
                                   :application.event/reviewed
                                   :application.event/reviewer-invited
@@ -642,14 +625,14 @@
     :else
     :visibility/public))
 
-(defn enrich-event [event get-user get-catalogue-item get-resource get-config]
+(defn enrich-event [event get-user get-catalogue-item get-resource]
   (let [event-type (:event/type event)]
     (merge event
            {:event/actor-attributes (get-user (:event/actor event))
             :event/visibility (get-event-visibility event)}
            (case event-type
              :application.event/resources-changed
-             {:application/resources (enrich-resources (:application/resources event) get-catalogue-item get-resource get-config)}
+             {:application/resources (enrich-resources (:application/resources event) get-catalogue-item get-resource)}
 
              :application.event/decision-requested
              {:application/deciders (mapv get-user (:application/deciders event))}
@@ -721,15 +704,10 @@
             :application/members
             enrich-members)))
 
-(defn get-workflow-handlers [workflow-id get-workflow get-user]
-  (let [workflow (get-workflow workflow-id)
+(defn enrich-workflow-handlers [application get-workflow get-user]
+  (let [workflow (get-workflow (get-in application [:application/workflow :workflow/id]))
         handlers (->> (get-in workflow [:workflow :handlers])
-                      (mapv (comp get-user :userid)))]
-    handlers))
-
-(defn enrich-workflow-handlers [application get-workflow-handlers]
-  (let [workflow-id (get-in application [:application/workflow :workflow/id])
-        handlers (get-workflow-handlers workflow-id)
+                      (mapv (comp get-user :userid)))
         active-users (set (map :event/actor (:application/events application)))
         handlers (map (fn [handler]
                         (if (contains? active-users (:userid handler))
@@ -840,6 +818,11 @@
       :else
       #{:handler})))
 
+(defn- enrich-attachments [application get-user]
+  (->> application
+       (transform [:application/attachments ALL] #(update % :attachment/user get-user))
+       (transform [:application/attachments ALL] #(assoc % :attachment/redact-roles (allowed-redact-roles application %)))))
+
 (defn- enrich-workflow-anonymize-handling [application get-workflow]
   (let [workflow-id (get-in application [:application/workflow :workflow/id])
         workflow (get-workflow workflow-id)
@@ -885,25 +868,6 @@
     (-> application
         (assoc :application/invited-members (set members)))))
 
-(defn- enrich-events [application get-user get-catalogue-item get-resource get-config]
-  (let [events (mapv #(enrich-event % get-user get-catalogue-item get-resource get-config)
-                     (:application/events application))]
-    (assoc application :application/events events)))
-
-(defn- enrich-attachments [application get-attachments-for-application get-user]
-  (->> (update application
-               :application/attachments
-               #(merge-lists-by :attachment/id
-                                %
-                                (get-attachments-for-application (getx application :application/id))))
-       (transform [:application/attachments ALL] #(update % :attachment/user get-user))
-       (transform [:application/attachments ALL] #(assoc % :attachment/redact-roles (allowed-redact-roles application %)))))
-
-(defn- enrich-applicant [application get-user]
-  (assoc application
-         :application/applicant
-         (get-user (get-in application [:application/applicant :userid]))))
-
 (defn enrich-with-injections
   [application {:keys [blacklisted?
                        get-attachments-for-application
@@ -914,30 +878,30 @@
                        get-resource
                        get-user
                        get-users-with-role
-                       get-workflow
-                       cached-get-workflow-handlers]
+                       get-workflow]
                 :as _injections}]
   (-> application
-      (enrich-application-forms get-form-template)
+      (update :application/forms enrich-forms get-form-template)
       enrich-answers ; uses enriched form
       enrich-field-visible ; uses enriched answers
-      enrich-application-description ; uses enriched answers
-      (enrich-application-resources get-catalogue-item get-resource get-config)
-      (enrich-duos get-config) ; uses enriched resources
+      set-application-description ; uses enriched answers
+      (update :application/resources enrich-resources get-catalogue-item get-resource)
+      (hide-duos-if-not-enabled get-config)
+      enrich-duos ; uses enriched resources
       (enrich-workflow-licenses get-workflow)
-      (enrich-application-licenses get-license)
-      (enrich-events get-user get-catalogue-item get-resource get-config)
-      (enrich-applicant get-user)
+      (update :application/licenses enrich-licenses get-license)
+      (update :application/events (partial mapv #(enrich-event % get-user get-catalogue-item get-resource)))
+      (assoc :application/applicant (get-user (get-in application [:application/applicant :userid])))
+      (update :application/attachments #(merge-lists-by :attachment/id % (get-attachments-for-application (getx application :application/id))))
       (enrich-user-attributes get-user)
       (enrich-blacklist blacklisted?) ; uses enriched users
-      (enrich-workflow-handlers (or cached-get-workflow-handlers
-                                    #(get-workflow-handlers % get-workflow get-user)))
+      (enrich-workflow-handlers get-workflow get-user)
       (enrich-workflow-anonymize-handling get-workflow)
       (enrich-deadline get-config)
       (enrich-super-users get-users-with-role)
       (enrich-workflow-disable-commands get-config get-workflow)
       (enrich-workflow-voting get-config get-workflow)
-      (enrich-attachments get-attachments-for-application get-user) ; users enriched roles
+      (enrich-attachments get-user)
       (enrich-processing-states get-config get-workflow)
       enrich-invited-members))
 
diff --git a/src/clj/rems/cache.clj b/src/clj/rems/cache.clj
index 890996e3d..19a4e59ab 100644
--- a/src/clj/rems/cache.clj
+++ b/src/clj/rems/cache.clj
@@ -185,14 +185,11 @@
         (logr/debug "<" id :evict k)))))
 
 (defn basic [{:keys [depends-on id miss-fn reload-fn]}]
-  (when (contains? @caches id)
-    (if (:dev rems.config/env)
-      (logr/warnf "overriding cache id %s" id)
-      (assert false (format "error overriding cache id %s" id))))
+  (assert (not (contains? @caches id)) (format "error overriding cache id %s" id))
   (let [initialized? false
         statistics (if (:dev rems.config/env)
-                     initial-statistics
-                     (select-keys initial-statistics [:reload :upsert :evict])) ; :get statistics can become big quickly
+                     (select-keys initial-statistics [:reload :upsert :evict]) ; :get statistics can become big quickly
+                     initial-statistics)
         the-cache (w/basic-cache-factory {})
         cache (->RefreshableCache id
                                   (atom statistics)
diff --git a/src/clj/rems/css/styles.clj b/src/clj/rems/css/styles.clj
index 2ff8dec73..5a55d23fc 100644
--- a/src/clj/rems/css/styles.clj
+++ b/src/clj/rems/css/styles.clj
@@ -219,11 +219,7 @@
     (for [i (range 10)]
       [(str ".bg-depth-" i) {:background-color (str "rgba(0,0,0," (/ i 30.0) ")")}])
     (for [i (range 10)]
-      [(str ".fs-depth-" i) {:font-size (u/rem (Math/max 1.0 (case i
-                                                               0 1.75
-                                                               1 1.5
-                                                               2 1.375
-                                                               (- 1.25 (* 0.05 (- i 3))))))}])
+      [(str ".fs-depth-" i) {:font-size (str (format "%.2f" (+ 0.75 (Math/pow 2 (- i)))) "rem")}])
     (for [i (range 10)]
       [(str ".pad-depth-" i) {:padding-left (u/rem (* 1.8 i))}])]
 
@@ -240,12 +236,7 @@
    ;;   which means that it should have a good contrast with light background.
    ;;   This could be made explicit by changing the name accordingly.
    [:.text-highlight {:color (theme-getx :color3)
-                      :font-weight "bold"}]
-   ;; copies Bootstrap header (h1, h2, ...) styles for non-header element
-   [:.tree-header {:font-weight 500
-                   :line-height 1.2
-                   :margin-top 0
-                   :margin-bottom 0}]))
+                      :font-weight "bold"}]))
 
 (defn- form-group []
   {:position "relative"
diff --git a/src/clj/rems/db/applications.clj b/src/clj/rems/db/applications.clj
index dc2de04ed..d1000bfdd 100644
--- a/src/clj/rems/db/applications.clj
+++ b/src/clj/rems/db/applications.clj
@@ -1,7 +1,6 @@
 (ns rems.db.applications
   "Query functions for forms and applications."
-  (:require [clojure.core.memoize :refer [memo]]
-            [clojure.java.jdbc :as jdbc]
+  (:require [clojure.java.jdbc :as jdbc]
             [clojure.set :as set]
             [clojure.test :refer [deftest is]]
             [clojure.tools.logging :as log]
@@ -200,42 +199,25 @@
             :bar #{"user-1"}}
            (group-users-by-role apps)))))
 
-(defn- user-roles-frequencies [applications]
-  (if (seq applications)
-    (let [all-user-roles (->> applications
-                              (eduction (map val)
-                                        (mapcat :application/user-roles)))
-          roles-by-user (reduce (fn [m [user roles]]
-                                  (assoc! m
-                                          ::users (conj! (::users m) user)
-                                          user (reduce conj!
-                                                       (or (m user) (transient []))
-                                                       roles)))
-                                (transient {::users (transient #{})})
-                                all-user-roles)
-          users (persistent! (::users roles-by-user))]
-      (-> (reduce (fn [m user]
-                    (assoc! m user (frequencies (persistent! (m user)))))
-                  roles-by-user
-                  users)
-          (dissoc! ::users)
-          persistent!))
-    {}))
-
 (defn- update-user-roles [updated-users old-roles-by-user old-updated-enriched-apps new-updated-enriched-apps]
-  (let [all-old-app-roles (user-roles-frequencies old-updated-enriched-apps)
-        all-new-app-roles (user-roles-frequencies new-updated-enriched-apps)]
-
-    (into old-roles-by-user
-          (for [userid updated-users
-                :let [old-user-roles (or (old-roles-by-user userid) {})
-                      old-app-roles (or (all-old-app-roles userid) {})
-                      new-app-roles (or (all-new-app-roles userid) {})]]
-
-            [userid (as-> old-user-roles roles
-                      (merge-with - roles old-app-roles)
-                      (merge-with + roles new-app-roles)
-                      (filter-vals pos? roles))]))))
+  (into old-roles-by-user
+        (for [userid updated-users
+              :let [old-user-roles (get old-roles-by-user userid {})
+                    old-app-roles (->> old-updated-enriched-apps
+                                       vals
+                                       (eduction (map :application/user-roles)
+                                                 (mapcat #(get % userid)))
+                                       frequencies)
+                    new-app-roles (->> new-updated-enriched-apps
+                                       vals
+                                       (eduction (map :application/user-roles)
+                                                 (mapcat #(get % userid)))
+                                       frequencies)]]
+
+          [userid (as-> old-user-roles roles
+                    (merge-with - roles old-app-roles)
+                    (merge-with + roles new-app-roles)
+                    (filter-vals pos? roles))])))
 
 (deftest update-user-roles-test
   (is (= {"alice" {:applicant 1} "bob" {:applicant 1}}
@@ -274,16 +256,6 @@
   ;; - deleted      - applications that are going away
   (let [updated-app-ids (set (into updated-app-ids deleted-app-ids)) ; let's consider deleted to be automatically an app to update
 
-        ;; temporarily cached injections because a mass update may have a lot of same calls (e.g. handlers of the workflow of many applications)
-        cached-get-user (memo (:get-user fetcher-injections))
-        cached-get-workflow-handlers (memo (fn [workflow-id]
-                                             (model/get-workflow-handlers workflow-id
-                                                                          (:get-workflow fetcher-injections)
-                                                                          cached-get-user)))
-        cached-injections (assoc fetcher-injections
-                                 :get-user cached-get-user
-                                 :cached-get-workflow-handlers cached-get-workflow-handlers)
-
         old-raw-apps (::raw-apps state)
         old-enriched-apps (::enriched-apps state)
         old-updated-enriched-apps (select-keys old-enriched-apps updated-app-ids)
@@ -297,7 +269,7 @@
                        (apply dissoc apps deleted-app-ids)
                        (doall apps))
         new-updated-raw-apps (select-keys new-raw-apps updated-app-ids)
-        new-updated-enriched-apps (doall (map-vals #(model/enrich-with-injections % cached-injections) new-updated-raw-apps))
+        new-updated-enriched-apps (doall (map-vals #(model/enrich-with-injections % fetcher-injections) new-updated-raw-apps))
         new-enriched-apps (as-> new-updated-enriched-apps apps
                             (merge old-enriched-apps apps)
                             (apply dissoc apps deleted-app-ids)
@@ -408,7 +380,7 @@
   (refresh-all-applications-cache!)
   (log/info "Finished rems.db.applications/reload-cache!"))
 
-(defn reload-applications! [{:keys [by-userids by-workflow-ids by-catalogue-item-ids]}]
+(defn reload-applications! [{:keys [by-userids by-workflow-ids]}]
   ;; NB: try make sure the cache is up to date so we have any new applications present
   (when (seq by-userids)
     (let [apps (refresh-all-applications-cache!)
@@ -432,21 +404,6 @@
       (when (seq app-ids)
         (update-in-all-applications-cache! app-ids))))
 
-  (when (seq by-catalogue-item-ids)
-    (let [apps (refresh-all-applications-cache!)
-          cat-ids (set by-catalogue-item-ids)
-          find-catalogue-item (fn [resources]
-                                (some #(contains? cat-ids (:catalogue-item/id %)) resources))
-          app-ids (->> (::enriched-apps apps)
-                       vals
-                       (filter (comp find-catalogue-item :application/resources))
-                       (mapv :application/id))]
-      (log/info "Reloading" (count app-ids) "applications because of catalogue item changes")
-      (when (seq app-ids)
-        (update-in-all-applications-cache! app-ids))))
-
-  (log/info "Finished reloading applications")
-
   nil)
 
 (defn delete-application!
diff --git a/src/clj/rems/db/user_settings.clj b/src/clj/rems/db/user_settings.clj
index a7dd8fd18..4d27cebc0 100644
--- a/src/clj/rems/db/user_settings.clj
+++ b/src/clj/rems/db/user_settings.clj
@@ -39,27 +39,22 @@
                        (assoc-some :notification-email (:notification-email data)))]
       (coerce-DbUserSettings settings))))
 
-(defn- merge-defaults [settings]
-  (merge (default-settings)
-         settings))
-
 (def user-settings-cache
   (cache/basic {:id ::user-settings-cache
                 :miss-fn (fn [userid]
                            (if-let [settings (db/get-user-settings {:user userid})]
                              (-> settings
                                  first
-                                 parse-user-settings-raw
-                                 merge-defaults)
+                                 parse-user-settings-raw)
                              cache/absent))
                 :reload-fn (fn []
                              (->> (db/get-user-settings {})
                                   (group-by :userid)
-                                  (map-vals parse-user-settings-raw)
-                                  (map-vals merge-defaults)))}))
+                                  (map-vals parse-user-settings-raw)))}))
 
 (defn get-user-settings [userid]
-  (cache/lookup-or-miss! user-settings-cache userid))
+  (merge (default-settings)
+         (cache/lookup-or-miss! user-settings-cache userid)))
 
 (defn validate-new-settings
   "Validates the new settings.
diff --git a/src/clj/rems/email/core.clj b/src/clj/rems/email/core.clj
index 7caf3dad5..f5f9c2e6f 100644
--- a/src/clj/rems/email/core.clj
+++ b/src/clj/rems/email/core.clj
@@ -33,8 +33,7 @@
       (template/event-to-emails (rems.application.model/enrich-event event
                                                                      rems.db.users/get-user
                                                                      rems.db.catalogue/get-catalogue-item
-                                                                     rems.db.resource/get-resource
-                                                                     (fn get-config [] env))
+                                                                     rems.db.resource/get-resource)
                                 (rems.db.applications/get-application app-id)))))
 
 (defn generate-event-emails! [new-events]
diff --git a/src/clj/rems/permissions.clj b/src/clj/rems/permissions.clj
index 1d5076f26..e6cec2d9f 100644
--- a/src/clj/rems/permissions.clj
+++ b/src/clj/rems/permissions.clj
@@ -1,16 +1,19 @@
 (ns rems.permissions
-  (:require [clojure.core.memoize :refer [memo]]
-            [clojure.set :as set]
+  (:require [clojure.set :as set]
             [clojure.test :refer [deftest is testing]]
-            [medley.core :refer [map-vals]]))
+            [medley.core :refer [map-vals map-kv-vals]]
+            [rems.common.util :refer [conj-set]]))
 
-(defn give-role-to-users [application role users]
+(defn- give-role-to-user [application role user]
   (assert (keyword? role) {:role role})
-  (assoc application :application/user-roles
-         (persistent! (reduce (fn [user-roles user]
-                                (assoc! user-roles user (conj (set (user-roles user)) role)))
-                              (transient (or (:application/user-roles application) {}))
-                              users))))
+  (assert (string? user) {:user user})
+  (update-in application [:application/user-roles user] conj-set role))
+
+(defn give-role-to-users [application role users]
+  (reduce (fn [app user]
+            (give-role-to-user app role user))
+          application
+          users))
 
 (defn- dissoc-if-empty [m k]
   (if (empty? (get m k))
@@ -33,22 +36,22 @@
   (testing "give first role"
     (is (= {:application/user-roles {"user" #{:role-1}}}
            (-> {}
-               (give-role-to-users :role-1 ["user"])))))
+               (give-role-to-user :role-1 "user")))))
   (testing "give more roles"
     (is (= {:application/user-roles {"user" #{:role-1 :role-2}}}
            (-> {}
-               (give-role-to-users :role-1 ["user"])
-               (give-role-to-users :role-2 ["user"])))))
+               (give-role-to-user :role-1 "user")
+               (give-role-to-user :role-2 "user")))))
   (testing "remove some roles"
     (is (= {:application/user-roles {"user" #{:role-1}}}
            (-> {}
-               (give-role-to-users :role-1 ["user"])
-               (give-role-to-users :role-2 ["user"])
+               (give-role-to-user :role-1 "user")
+               (give-role-to-user :role-2 "user")
                (remove-role-from-user :role-2 "user")))))
   (testing "remove all roles"
     (is (= {:application/user-roles {}}
            (-> {}
-               (give-role-to-users :role-1 ["user"])
+               (give-role-to-user :role-1 "user")
                (remove-role-from-user :role-1 "user")))))
   (testing "give a role to multiple users"
     (is (= {:application/user-roles {"user-1" #{:role-1}
@@ -57,19 +60,12 @@
                (give-role-to-users :role-1 ["user-1" "user-2"])))))
   (testing "multiple users, get the roles of a single user"
     (let [app (-> {}
-                  (give-role-to-users :role-1 ["user-1"])
-                  (give-role-to-users :role-2 ["user-2"]))]
+                  (give-role-to-user :role-1 "user-1")
+                  (give-role-to-user :role-2 "user-2"))]
       (is (= #{:role-1} (user-roles app "user-1")))
       (is (= #{:role-2} (user-roles app "user-2")))
       (is (= #{:everyone-else} (user-roles app "user-3"))))))
 
-(defn- set-role-permissions [app-role-permissions permission-map]
-  (reduce-kv (fn [m role permissions]
-               (assert (keyword? role) {:role role})
-               (assoc m role (set permissions)))
-             app-role-permissions
-             permission-map))
-
 (defn update-role-permissions
   "Sets role specific permissions for the application.
 
@@ -79,7 +75,11 @@
    or they may be used to specify whether the user can see all events and
    comments from the reviewers (e.g. `:see-everything`)."
   [application permission-map]
-  (update application :application/role-permissions set-role-permissions permission-map))
+  (reduce (fn [application [role permissions]]
+            (assert (keyword? role) {:role role})
+            (assoc-in application [:application/role-permissions role] (set permissions)))
+          application
+          permission-map))
 
 (deftest test-update-role-permissions
   (testing "adding"
@@ -122,7 +122,11 @@
   (assert (keyword? permission)
           {:message "invalid permission" :rule rule}))
 
-(defn- validate-and-compile [rules]
+(defn compile-rules
+  "Compiles rules of the format `[{:role keyword :permission keyword}]`
+  to the format expected by the `whitelist` and `blacklist` functions.
+  If `:role` is missing or nil, it means every role."
+  [rules]
   (doseq [rule rules]
     (validate-rule rule))
   (->> rules
@@ -130,34 +134,19 @@
        (map-vals (fn [rules]
                    (set (map :permission rules))))))
 
-(def compile-rules
-  "Compiles rules of the format `[{:role keyword :permission keyword}]`
-  to the format expected by the `whitelist` and `blacklist` functions.
-  If `:role` is missing or nil, it means every role."
-  (memo validate-and-compile))
+(defn- map-permissions [application f]
+  (update application :application/role-permissions #(map-kv-vals f %)))
 
 (defn- permissions-for-role [rules role]
-  (let [role-permissions (or (rules role) #{})]
-    (if-some [every-permissions (rules nil)]
-      (set/union role-permissions
-                 every-permissions)
-      role-permissions)))
-
-(defn- blacklist-permissions [role-permissions rules]
-  (reduce-kv (fn [m role permissions]
-               (assoc m role (if-some [role-permissions (permissions-for-role rules role)]
-                               (set/difference permissions role-permissions)
-                               permissions)))
-             {}
-             role-permissions))
-
-(def ^:private memoized-blacklist-permissions (memo blacklist-permissions))
+  (set/union (get rules role #{})
+             (get rules nil #{})))
 
 (defn blacklist
   "Applies rules for restricting the possible permissions.
   `rules` is what `compile-rules` returns."
   [application rules]
-  (update application :application/role-permissions memoized-blacklist-permissions rules))
+  (map-permissions application (fn [role permissions]
+                                 (set/difference permissions (permissions-for-role rules role)))))
 
 (deftest test-blacklist
   (let [app (-> {}
@@ -177,21 +166,12 @@
              (blacklist app (compile-rules [{:role :role-1 :permission :foo}
                                             {:role :role-2 :permission :bar}])))))))
 
-(defn- whitelist-permissions [role-permissions rules]
-  (reduce-kv (fn [m role permissions]
-               (assoc m role (if-some [role-permissions (permissions-for-role rules role)]
-                               (set/intersection permissions role-permissions)
-                               permissions)))
-             {}
-             role-permissions))
-
-(def ^:private memoized-whitelist-permissions (memo whitelist-permissions))
-
 (defn whitelist
   "Applies rules for restricting the possible permissions.
   `rules` is what `compile-rules` returns."
   [application rules]
-  (update application :application/role-permissions memoized-whitelist-permissions rules))
+  (map-permissions application (fn [role permissions]
+                                 (set/intersection permissions (permissions-for-role rules role)))))
 
 (deftest test-whitelist
   (let [app (-> {}
@@ -229,14 +209,14 @@
   (testing "one role"
     (is (= #{:foo}
            (-> {}
-               (give-role-to-users :role-1 ["user"])
+               (give-role-to-user :role-1 "user")
                (update-role-permissions {:role-1 #{:foo}})
                (user-permissions "user")))))
   (testing "multiple roles"
     (is (= #{:foo :bar}
            (-> {}
-               (give-role-to-users :role-1 ["user"])
-               (give-role-to-users :role-2 ["user"])
+               (give-role-to-user :role-1 "user")
+               (give-role-to-user :role-2 "user")
                (update-role-permissions {:role-1 #{:foo}
                                          :role-2 #{:bar}})
                (user-permissions "user"))))))
diff --git a/src/clj/rems/service/catalogue.clj b/src/clj/rems/service/catalogue.clj
index 21d20f773..df06b1a68 100644
--- a/src/clj/rems/service/catalogue.clj
+++ b/src/clj/rems/service/catalogue.clj
@@ -115,20 +115,18 @@
   (rems.db.catalogue/edit-catalogue-item! id {:categories (:categories item)
                                               :localizations localizations
                                               :organization-id (:organization/id organization)})
-  (rems.db.applications/reload-applications! {:by-catalogue-item-ids [id]})
+  (rems.db.applications/reload-cache!)
   {:success true})
 
 (defn set-catalogue-item-enabled! [{:keys [id enabled]}]
   (check-allowed-to-edit! id)
   (rems.db.catalogue/set-attributes! id {:enabled enabled})
-  (rems.db.applications/reload-applications! {:by-catalogue-item-ids [id]})
   {:success true})
 
 (defn set-catalogue-item-archived! [{:keys [id archived]}]
   (check-allowed-to-edit! id)
   (or (dependencies/change-archive-status-error archived {:catalogue-item/id id})
       (do (rems.db.catalogue/set-attributes! id {:archived archived})
-          (rems.db.applications/reload-applications! {:by-catalogue-item-ids [id]})
           {:success true})))
 
 (defn change-form!
@@ -154,9 +152,6 @@
       ;; hide the old catalogue item
       (rems.db.catalogue/set-attributes! (:id item) {:archived true :enabled false})
 
-      ;; notify applications with old catalogue item
-      (rems.db.applications/reload-applications! {:by-catalogue-item-ids [(:id item)]})
-
       {:success true :catalogue-item-id new-item-id})))
 
 (defn update!
@@ -196,7 +191,4 @@
       ;; hide the old catalogue item
       (rems.db.catalogue/set-attributes! (:id item) {:archived true :enabled false})
 
-      ;; notify applications with old catalogue item
-      (rems.db.applications/reload-applications! {:by-catalogue-item-ids [(:id item)]})
-
       {:success true :catalogue-item-id new-item-id})))
diff --git a/src/cljs/rems/administration/status_flags.cljs b/src/cljs/rems/administration/status_flags.cljs
index b295bfb40..93af517ed 100644
--- a/src/cljs/rems/administration/status_flags.cljs
+++ b/src/cljs/rems/administration/status_flags.cljs
@@ -11,22 +11,6 @@
       (:title item)
       (localized (:organization/name item))))
 
-;; XXX: consider rename to enabled-toggle-button
-(defn enabled-toggle
-  ([item on-change] (enabled-toggle {} item on-change))
-  ([opts item on-change]
-   (let [enabled? (:enabled item false)
-         label (if enabled?
-                 :t.administration/disable
-                 :t.administration/enable)]
-     [:button.btn.btn-primary.button-min-width
-      {:id (:id opts)
-       :type :button
-       :on-click #(on-change (update item :enabled not)
-                             [:span [text label]
-                              " \""  [get-localized-title-for-anything item] "\""])}
-      (text label)])))
-
 (defn enabled-toggle-action [{:keys [id on-change]} item]
   (let [enabled? (:enabled item false)
         label (if enabled?
@@ -39,22 +23,6 @@
                             " \""  [get-localized-title-for-anything item] "\""])
      :label [text label]}))
 
-;; XXX: consider rename to archived-toggle-button
-(defn archived-toggle
-  ([item on-change] (archived-toggle {} item on-change))
-  ([opts item on-change]
-   (let [archived? (:archived item false)
-         label (if archived?
-                 :t.administration/unarchive
-                 :t.administration/archive)]
-     [:button.btn.btn-primary.button-min-width
-      {:id (:id opts)
-       :type :button
-       :on-click #(on-change (update item :archived not)
-                             [:span [text label]
-                              " \"" [get-localized-title-for-anything item] "\""])}
-      (text label)])))
-
 (defn archived-toggle-action [{:keys [id on-change]} item]
   (let [archived? (:archived item false)
         label (if archived?
diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs
index c24c63f21..dc8b8807c 100644
--- a/src/cljs/rems/atoms.cljs
+++ b/src/cljs/rems/atoms.cljs
@@ -242,14 +242,20 @@
                         (dissoc props :text))
      (:text props)]))
 
-(defn- wrap-rate-limit [callback ms]
-  (r/with-let [wait (r/atom nil)]
-    (fn rate-limited-callback [& args]
-      (when-not @wait
-        (reset! wait (js/setTimeout #(reset! wait nil) ms))
-        (apply callback args)))
-    (finally
-      (some-> @wait js/clearTimeout))))
+(def ^:private rate-limited (atom {}))
+
+(let [idx (atom 0)]
+  (defn- rate-limited-id! []
+    (swap! idx inc)))
+
+(defn- wrap-rate-limit [callback ms id]
+  (fn rate-limited-callback [& args]
+    (when-not (contains? @rate-limited id)
+      (swap! rate-limited
+             assoc
+             id
+             (js/setTimeout #(swap! rate-limited dissoc id) ms))
+      (apply callback args))))
 
 (defn rate-limited-button
   "atoms/button variant that allows click handler to be called, at most,
@@ -258,11 +264,15 @@
 
    * `:wait` interval in milliseconds"
   [{:keys [disabled wait] :or {wait 2000} :as props}]
-  [button (-> props
-              (dissoc :wait
-                      :loading?
-                      (when disabled :on-click))
-              (update-existing :on-click wrap-rate-limit wait))])
+  (r/with-let [callback-id (rate-limited-id!)]
+    [button (-> props
+                (dissoc :wait
+                        :loading?
+                        (when disabled :on-click))
+                (update-existing :on-click wrap-rate-limit wait callback-id))]
+    (finally
+      (some-> (get @rate-limited callback-id) js/clearTimeout)
+      (swap! rate-limited dissoc callback-id))))
 
 (defn action-button
   "Takes an `action` description and creates a button that triggers it."
@@ -426,15 +436,18 @@
        (component-info rate-limited-action-button)
        (example "example command as rate-limited button"
 
-                (defn- stateful-context [[c cmd]]
+                (defn- stateful-context [& pairs]
                   (r/with-let [n (r/atom 0)
-                               on-click #(r/rswap! n inc)]
+                               on-click #(do (r/rswap! n inc) (println @n))]
                     [:<>
-                     [c (assoc cmd :on-click on-click)]
+                     (for [[c cmd] pairs]
+                       ^{:key (:id cmd)}
+                       [:span.ml-1 [c (assoc cmd :on-click on-click)]])
                      [:span.ml-2 "Count: " @n]]))
 
                 [stateful-context
-                 [rate-limited-action-button example-command]])
+                 [rate-limited-action-button example-command]
+                 [rate-limited-action-button (assoc example-command :id "second-rate-limited-button")]])
 
        (component-info commands)
        (example "empty commands" [commands])
diff --git a/src/cljs/rems/catalogue.cljs b/src/cljs/rems/catalogue.cljs
index 654c5d2a3..5117f8a84 100644
--- a/src/cljs/rems/catalogue.cljs
+++ b/src/cljs/rems/catalogue.cljs
@@ -1,7 +1,5 @@
 (ns rems.catalogue
   (:require [re-frame.core :as rf]
-            [reagent.core :as r]
-            [reagent.format :as rfmt]
             [rems.application-list :as application-list]
             [rems.common.application-util :refer [form-fields-editable?]]
             [rems.atoms :as atoms :refer [external-link document-title document-title]]
@@ -10,14 +8,12 @@
             [rems.fetcher :as fetcher]
             [rems.flash-message :as flash-message]
             [rems.common.roles :as roles]
-            [rems.common.util :refer [andstr]]
             [rems.config]
             [rems.globals]
             [rems.spinner :as spinner]
             [rems.table :as table]
             [rems.tree :as tree]
-            [rems.text :refer [text text-format get-localized-title localized]]
-            [rems.util :refer [get-dom-element]]))
+            [rems.text :refer [text text-format get-localized-title]]))
 
 (rf/reg-event-fx
  ::enter-page
@@ -71,35 +67,6 @@
    (str "/application?items=" (:id item))
    (text :t.cart/apply)])
 
-(defn- click-apply-button [row-key root-id]
-  (let [selector (rfmt/format "#%s [data-row='%s'] .apply-for-catalogue-item"
-                              root-id row-key)]
-    (some-> (get-dom-element selector)
-            .click)))
-
-(defn- row-command [item cart-item-ids]
-  (cond
-    (not (:enable-cart @rems.globals/config))
-    [apply-button item]
-
-    (contains? cart-item-ids (:id item))
-    [cart/remove-from-cart-button item]
-
-    :else
-    [cart/add-to-cart-button item]))
-
-(defn- perform-row-command [item cart-item-ids root-id]
-  (when-not (:category/id item)
-    (cond
-      (not (:enable-cart @rems.globals/config))
-      (click-apply-button (:id item) (name root-id))
-
-      (contains? cart-item-ids (:id item))
-      (rf/dispatch [:rems.cart/remove-item (:id item)])
-
-      :else
-      (rf/dispatch [:rems.cart/add-item item]))))
-
 (rf/reg-sub
  ::catalogue-table-rows
  (fn [_ _]
@@ -112,7 +79,12 @@
               :name {:value (get-localized-title item)}
               :commands {:display-value [:div.commands.flex-nowrap.justify-content-end
                                          [catalogue-item-more-info item]
-                                         [row-command item cart-item-ids]]}})
+                                         (when @roles/logged-in?
+                                           (if (:enable-cart @rems.globals/config)
+                                             (if (contains? cart-item-ids (:id item))
+                                               [cart/remove-from-cart-button item]
+                                               [cart/add-to-cart-button item])
+                                             (apply-button item)))]}})
            catalogue))))
 
 (defn draft-application-list []
@@ -140,27 +112,25 @@
                    :default-sort-column :name}])
 
 (defn- catalogue-tree []
-  (let [cart @(rf/subscribe [:rems.cart/cart])
+  (let [language @rems.config/current-language
+        cart @(rf/subscribe [:rems.cart/cart])
         cart-item-ids (set (map :id cart))
-        get-row-details-id (fn [id] 
-                             (str (name ::catalogue-tree) "-" id "-details"))
         catalogue {:id ::catalogue-tree
                    :row-key #(or (some->> (:category/id %) (str "category_"))
                                  (:id %))
                    :show-matching-parents? (:catalogue-tree-show-matching-parents @rems.globals/config)
                    :columns [{:key :name
-                              :value #(or (localized (:category/title %)) (get-localized-title %))
+                              :value #(or (get (:category/title %) language) (get-localized-title %))
                               :sort-value #(vector (:category/display-order % 2147483647) ; can't use Java Integer/MAX_VALUE here but anything that is not set is last
-                                                   (or (localized (:category/title %)) "_") ; "_" means not set i.e. item is last
+                                                   (get (:category/title %) language "_") ; "_" means not set i.e. item is last
                                                    (get-localized-title %))
                               :title (text :t.catalogue/header)
-                              :content #(if-let [id (:category/id %)]
+                              :content #(if (:category/id %)
                                           [:div.my-2
-                                           [:div.tree-header {:class (str "fs-depth-" (:depth % 0))}
-                                            (localized (:category/title %))]
-                                           (when-let [description (localized (:category/description %))]
-                                             [:div.mt-3 {:id (get-row-details-id id)}
-                                              description])]
+                                           [:h3.mb-0 {:class (str "fs-depth-" (:depth % 0))}
+                                            (get (:category/title %) language)]
+                                           (when-let [description (get (:category/description %) language)]
+                                             [:div.mt-3 description])]
                                           [:div (get-localized-title %)])
                               :col-span #(if (:category/id %) 2 1)}
                              {:key :commands
@@ -168,7 +138,11 @@
                                           [:div.commands.flex-nowrap.justify-content-end
                                            [catalogue-item-more-info %]
                                            (when @roles/logged-in?
-                                             [row-command % cart-item-ids])])
+                                             (if (:enable-cart @rems.globals/config)
+                                               (if (contains? cart-item-ids (:id %))
+                                                 [cart/remove-from-cart-button %]
+                                                 [cart/add-to-cart-button %])
+                                               (apply-button %)))])
                               :aria-label (text :t.actions/commands)
                               :sortable? false
                               :filterable? false}]
@@ -179,20 +153,7 @@
                                    true ; always pass categories
                                    (and (:enabled row)
                                         (not (:expired row)))))
-                   :row-action (fn [row]
-                                 (when @roles/logged-in?
-                                   (perform-row-command (:value row) cart-item-ids ::catalogue-tree)))
-                   :row-aria-label (fn [{:keys [value] :as row}]
-                                     (cond
-                                       (:category/id value) (str (text :t.administration/category)
-                                                                 (andstr ": " (localized (:category/title value))))
-                                       :else (str (text :t.administration/resource)
-                                                  (andstr ": " (get-localized-title value)))))
-                   :row-aria-describedby (fn [{:keys [value] :as row}]
-                                           (when-let [id (:category/id value)]
-                                             (get-row-details-id id)))
-                   :default-sort-column :name
-                   :aria-labelledby :catalogue-apply-resources}]
+                   :default-sort-column :name}]
     [:div.mt-2rem
      [tree/search catalogue]
      [tree/tree catalogue]]))
@@ -212,7 +173,7 @@
                        @(rf/subscribe [::full-catalogue-tree :fetching?]))
            [cart/cart-list-container]))
 
-       [:h2#catalogue-apply-resources (text :t.catalogue/apply-resources)]])
+       [:h2 (text :t.catalogue/apply-resources)]])
 
     (when (:enable-catalogue-tree @rems.globals/config)
       (if @(rf/subscribe [::full-catalogue :fetching?])
diff --git a/src/cljs/rems/focus.cljs b/src/cljs/rems/focus.cljs
index 1ea8d3aa1..a069e3e4a 100644
--- a/src/cljs/rems/focus.cljs
+++ b/src/cljs/rems/focus.cljs
@@ -3,10 +3,15 @@
   (:require [rems.util :refer [get-bounding-client-rect get-dom-element on-element-appear]]))
 
 (defn focus [el-or-selector & [opts]]
-  (doto (get-dom-element el-or-selector)
-    (.setAttribute "tabindex" "-1")
-    (.focus (clj->js (or opts {})))
-    (.removeAttribute "tabindex")))
+  (let [el (get-dom-element el-or-selector)
+        tabindex (.getAttribute el "tabindex")
+        restore-tabindex (if tabindex
+                           #(.setAttribute % "tabindex" tabindex)
+                           #(.removeAttribute % "tabindex"))]
+    (doto el
+      (.setAttribute "tabindex" "-1")
+      (.focus (clj->js (or opts {})))
+      restore-tabindex)))
 
 (defn focus-without-scroll [el-or-selector]
   (focus el-or-selector (js-obj "preventScroll" true)))
diff --git a/src/cljs/rems/tree.cljs b/src/cljs/rems/tree.cljs
index 58535ad46..8c64d9eca 100644
--- a/src/cljs/rems/tree.cljs
+++ b/src/cljs/rems/tree.cljs
@@ -23,13 +23,11 @@
   (:require [clojure.string :as str]
             [goog.functions :refer [debounce]]
             [re-frame.core :as rf]
-            [reagent.format :as rfmt]
             [rems.atoms :refer [sort-symbol]]
             [rems.common.util :refer [conj-vec index-by]]
             [rems.config]
             [rems.guide-util :refer [component-info example namespace-info]]
             [rems.search :as search]
-            [rems.util :refer [get-dom-element]]
             [schema.core :as s]))
 
 (s/defschema ColumnKey
@@ -78,17 +76,11 @@
   {;; Unique ID for this tree, preferably a namespaced keyword.
    ;; Used not only as a HTML element ID, but also for separating
    ;; the internal state of different trees in the re-frame db.
-   :id (s/cond-pre s/Keyword s/Str)
+   :id s/Keyword
    ;; The columns of the table, in the order that they should be shown.
    ;; Optional function (Row -> key value) for the React key value
    ;; The default is from :key.
    (s/optional-key :row-key) s/Any
-   ;; Optional function (Row -> void) for performing default action via keyboard.
-   (s/optional-key :row-action) s/Any
-   ;; Optional function (Row -> string) for row aria-label.
-   (s/optional-key :row-aria-label) s/Any
-   ;; Optional function (Row -> string) for row aria-describedby.
-   (s/optional-key :row-aria-describedby) s/Any
    :columns [{;; Reference to a column in `rems.tree/Row` of `:rows`.
               :key ColumnKey
               ;; Title to show at the top of the column.
@@ -147,10 +139,7 @@
    (s/optional-key :tr-class) s/Any
    ;; Default sorting column and order.
    (s/optional-key :default-sort-column) (s/maybe ColumnKey)
-   (s/optional-key :default-sort-order) (s/maybe (s/enum :asc :desc))
-   ;; Optional assistive technology attributes.
-   (s/optional-key :aria-label) s/Str
-   (s/optional-key :aria-labelledby) (s/cond-pre s/Str s/Keyword)})
+   (s/optional-key :default-sort-order) (s/maybe (s/enum :asc :desc))})
 
 (defn apply-row-defaults [tree row]
   (let [children ((:children tree :children) row)
@@ -412,122 +401,25 @@
                (when (= (:key column) (:sort-column sorting))
                  [sort-symbol (:sort-order sorting)]))]))))
 
-(defn- focus-previous [row-key tree-id]
-  (let [selector (rfmt/format "#%s [data-row='%s']" tree-id row-key)]
-    (some-> (get-dom-element selector)
-            .-previousSibling
-            .focus)))
-
-(defn- focus-next [row-key tree-id]
-  (let [selector (rfmt/format "#%s [data-row='%s']" tree-id row-key)]
-    (some-> (get-dom-element selector)
-            .-nextSibling
-            .focus)))
-
-(defn- focus-parent [parent-id tree-id]
-  (when parent-id
-    (let [selector (rfmt/format "#%s [data-row='%s']" tree-id parent-id)]
-      (some-> (get-dom-element selector)
-              .focus))))
-
-(defn- focus-root [tree-id]
-  (let [selector (rfmt/format "#%s [data-row]:first-child" tree-id)]
-    (some-> (get-dom-element selector)
-            .focus)))
-
-(defn- focus-last [tree-id]
-  (let [selector (rfmt/format "#%s [data-row]:last-child" tree-id)]
-    (some-> (get-dom-element selector)
-            .focus)))
-
-(def ^:private key-shortcuts
-  #{"ArrowRight" "ArrowLeft" "ArrowDown" "ArrowUp" "Home" "End" "Enter"})
-
 (defn- tree-row [row tree]
-  (let [element-ref (atom nil) ; not reactive, only used in keydown handler
-        row-key (:key row)
-        tree-id (:id tree)
-        parents (:parents row)
-        children (:children row)
-        expanded (when (seq children)
-                   @(rf/subscribe [::expanded-row tree row-key]))
-        toggle-expanded (when (seq children)
-                          #(rf/dispatch [::toggle-row-expanded tree row-key]))]
-    (into [:tr {:data-row row-key
-                :class [(when-let [tr-class-fn (:tr-class tree)]
-                          (tr-class-fn (:value row)))
-                        (when (seq children)
-                          :clickable)]
-                :on-click toggle-expanded
-                :tabIndex 0
-                :aria-label (when-let [aria-label-fn (:row-aria-label tree)]
-                              (aria-label-fn row))
-                :aria-describedby (when-let [aria-details-fn (:row-aria-details tree)]
-                                    (aria-details-fn row))
-                :role :treeitem
-                :aria-selected false ; implicit attribute for <tr>, we don't want screen reader to say "selected" when tree rows cannot be selected
-                :aria-expanded expanded
-                :aria-level (inc (:depth row 0)) ; NB: level must start from 1
-                :ref #(reset! element-ref %)
-                :on-key-down (fn handle-keydown-event [^js e]
-                               ;; dont intercept tabbing
-                               (when (not= "Tab" (.-code e))
-                                 ;; accept all but enter from bubbled events
-                                 (when (or (contains? (disj key-shortcuts "Enter") (.-code e))
-                                           (= @element-ref (.-target e)))
-                                   (.preventDefault e) ; prevent event bubbling when acting on key shortcuts
-                                   (case (.-code e)
-                                     ;; When focus is on a closed node, opens the node; focus does not move.
-                                     ;; When focus is on an open node, moves focus to the first child node.
-                                     "ArrowRight" (cond
-                                                    (true? expanded) (focus-next row-key (name tree-id))
-                                                    (false? expanded) (toggle-expanded)
-                                                    :else nil)
-
-                                     ;; When focus is on an open node, closes the node.
-                                     ;; When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
-                                     "ArrowLeft" (if (true? expanded)
-                                                   (toggle-expanded)
-                                                   ;; last parent is the closest to this row
-                                                   (focus-parent (last parents) (name tree-id)))
-
-                                     ;; Moves focus to the next node that is focusable without opening or closing a node.
-                                     "ArrowDown" (focus-next row-key (name tree-id))
-
-                                     ;; Moves focus to the previous node that is focusable without opening or closing a node.
-                                     "ArrowUp" (focus-previous row-key (name tree-id))
-
-                                     ;; Moves focus to the first node in the tree without opening or closing a node.
-                                     "Home" (focus-root (name tree-id))
-
-                                     ;; Moves focus to the last node in the tree that is focusable without opening the node.
-                                     "End" (focus-last (name tree-id))
-
-                                     ;; Performs the default action of the currently focused node.
-                                     ;; For parent nodes, it opens or closes the node. 
-                                     ;; In single-select trees, if the node has no children, selects the current node if not already selected (which is the default action).
-                                     "Enter" (cond
-                                               (true? expanded) (toggle-expanded)
-                                               (false? expanded) (toggle-expanded)
-                                               :else
-                                               (when-let [row-action-fn (:row-action tree)]
-                                                 (row-action-fn row)))
-                                     nil))))}]
-          (for [column (:columns tree)]
-            (:td (get (:columns-by-key row) (:key column)))))))
+  (into [:tr {:data-row (:key row)
+              :class [(when-let [tr-class-fn (:tr-class tree)]
+                        (tr-class-fn (:value row)))
+                      (when (seq (:children row))
+                        :clickable)]
+              :on-click (when (seq (:children row))
+                          #(rf/dispatch [::toggle-row-expanded tree (:key row)]))}]
+        (for [column (:columns tree)]
+          (:td (get (:columns-by-key row) (:key column))))))
 
 (defn tree [tree]
   (let [rows @(rf/subscribe [::displayed-rows tree])]
     [:div.table-border ; TODO duplicate or generalize styles?
      [:table.rems-table {:id (name (:id tree))
-                         :class (:id tree)
-                         :aria-label (:aria-label tree)
-                         :aria-labelledby (:aria-labelledby tree)
-                         :aria-keyshortcuts (str/join " " (sort key-shortcuts))}
-      [:thead {:role :presentation}
+                         :class (:id tree)}
+      [:thead
        [tree-header tree]]
-      [:tbody {:key @rems.config/current-language ; performance optimization: rebuild instead of update existing components
-               :role :tree}
+      [:tbody {:key @rems.config/current-language} ; performance optimization: rebuild instead of update existing components
        (for [row rows]
          ^{:key (:react-key row)} ; row key can be duplicated because it's a DAG
          [tree-row row tree])]]]))
@@ -544,7 +436,7 @@
    (example "empty tree"
             (rf/reg-sub ::empty-tree-rows (fn [_ _] []))
 
-            [tree {:id "rems-tree-example0"
+            [tree {:id ::example0
                    :columns [{:key :first-name
                               :title "First name"
                               :sortable? false
@@ -554,8 +446,7 @@
                               :sortable? false
                               :filterable? false}]
                    :rows [::empty-tree-rows]
-                   :default-sort-column :first-name
-                   :aria-label "Empty tree"}])
+                   :default-sort-column :first-name}])
 
    (example "static tree with a three level hierarchy"
             (defn- example-commands [text]
@@ -589,7 +480,7 @@
 
             (rf/reg-sub ::example-tree-rows (fn [_ _] example-data))
 
-            [tree {:id "rems-tree-example1"
+            [tree {:id ::example1
                    :columns [{:key :name
                               :title "Name"
                               :sortable? false
@@ -599,14 +490,13 @@
                               :sortable? false
                               :filterable? false}]
                    :rows [::example-tree-rows]
-                   :default-sort-column :title
-                   :aria-label "Static tree with a three level hierarchy"}])
+                   :default-sort-column :title}])
 
    (example "sortable and filterable tree"
 
             [:p "Filtering and search can be added by using the " [:code "rems.tree/search"] " component"]
 
-            (let [example2 {:id "rems-tree-example2"
+            (let [example2 {:id ::example2
                             :show-matching-parents? true
                             :columns [{:key :name
                                        :title "Name"}
@@ -615,8 +505,7 @@
                                        :sortable? false
                                        :filterable? false}]
                             :rows [::example-tree-rows]
-                            :default-sort-column :title
-                            :aria-label "Sortable and filterable tree"}]
+                            :default-sort-column :title}]
               [:div
                [search example2]
                [tree example2]]))])


Test patch:
diff --git a/test/clj/rems/api/test_catalogue_items.clj b/test/clj/rems/api/test_catalogue_items.clj
index 6bf99d0d3..7170b73b7 100644
--- a/test/clj/rems/api/test_catalogue_items.clj
+++ b/test/clj/rems/api/test_catalogue_items.clj
@@ -1,37 +1,18 @@
 (ns ^:integration rems.api.test-catalogue-items
   (:require [clojure.string :as str]
-            [clojure.test :refer [deftest is testing use-fixtures]]
-            [clojure.tools.logging.test :as log-test]
-            [rems.api.testing :refer [api-call api-fixture authenticate read-body read-ok-body response-is-forbidden? response-is-not-found? response-is-unauthorized?]]
+            [clojure.test :refer :all]
+            [rems.api.testing :refer :all]
             [rems.db.applications]
             [rems.db.test-data-helpers :as test-helpers]
             [rems.db.testing :refer [owners-fixture +test-api-key+]]
             [rems.handler :refer [handler]]
-            [ring.mock.request :refer [json-body request]]))
+            [ring.mock.request :refer :all]))
 
 (use-fixtures
   :each
   api-fixture
   owners-fixture)
 
-(defmacro is-logged?
-  "Evaluates `body`, asserts `(is (clojure.tools.logging.test/logged? logger-ns level message))`,
-   and returns the result of `body`."
-  [logger-ns level message body]
-  `(clojure.tools.logging.test/with-log
-     (let [result# (do ~body)]
-       (is (clojure.tools.logging.test/logged? ~logger-ns ~level ~message))
-       result#)))
-
-(defmacro is-not-logged?
-  "Evaluates `body`, asserts `(is (not (clojure.tools.logging.test/logged? logger-ns level message)))`,
-   and returns the result of `body`."
-  [logger-ns level message body]
-  `(clojure.tools.logging.test/with-log
-     (let [result# (do ~body)]
-       (is (not (clojure.tools.logging.test/logged? ~logger-ns ~level ~message)))
-       result#)))
-
 (deftest catalogue-items-api-test
   (let [user-id "alice"
         form-id (test-helpers/create-form! {:form/internal-name "form name"
@@ -139,7 +120,6 @@
         form-id (test-helpers/create-form! {:organization {:organization/id "organization1"}})
         wf-id (test-helpers/create-workflow! {:organization {:organization/id "organization1"}})
         res-id (test-helpers/create-resource! {:organization {:organization/id "organization1"}})]
-
     (testing "create"
       (let [create (-> (request :post "/api/catalogue-items/create")
                        (authenticate +test-api-key+ owner)
@@ -160,7 +140,6 @@
           (is (= {:sv "http://info.se"}
                  (:catalogue-item/infourl
                   (first (:application/resources (get-app))))))
-
           (testing "... and fetch"
             (let [data (-> (request :get (str "/api/catalogue-items/" id))
                            (authenticate +test-api-key+ user)
@@ -173,26 +152,22 @@
               (is (= {:title "Sv title"
                       :infourl "http://info.se"}
                      (dissoc (get-in data [:localizations :sv]) :id :langcode)))))
-
           (testing "... and edit (as owner)"
-            (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                             (-> (request :put "/api/catalogue-items/edit")
-                                 (authenticate +test-api-key+ owner)
-                                 (json-body {:id id
-                                             :organization {:organization/id changed-organization1}
-                                             :localizations {:sv {:title "Sv title 2"
-                                                                  :infourl nil}
-                                                             :fi {:title "Fi title"
-                                                                  :infourl "http://info.fi"}}})
-                                 handler
-                                 read-ok-body))]
+            (let [response (-> (request :put "/api/catalogue-items/edit")
+                               (authenticate +test-api-key+ owner)
+                               (json-body {:id id
+                                           :organization {:organization/id changed-organization1}
+                                           :localizations {:sv {:title "Sv title 2"
+                                                                :infourl nil}
+                                                           :fi {:title "Fi title"
+                                                                :infourl "http://info.fi"}}})
+                               handler
+                               read-ok-body)]
               (is (:success response) (pr-str response))
-
               (testing "application is updated when catalogue item is edited"
                 (is (= {:fi "http://info.fi"}
                        (:catalogue-item/infourl
                         (first (:application/resources (get-app)))))))
-
               (testing "... and fetch"
                 (let [data (-> (request :get (str "/api/catalogue-items/" id))
                                (authenticate +test-api-key+ user)
@@ -209,80 +184,52 @@
                   (is (= {:title "Fi title"
                           :infourl "http://info.fi"}
                          (dissoc (get-in data [:localizations :fi]) :id :langcode)))))))
-
           (testing "... and edit (as organization owner)"
-            (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                             (-> (request :put "/api/catalogue-items/edit")
-                                 (authenticate +test-api-key+ "organization-owner1")
-                                 (json-body {:id id
-                                             :organization {:organization/id changed-organization2}
-                                             :localizations {:sv {:title "Sv title 2"
-                                                                  :infourl nil}
-                                                             :fi {:title "Fi title 2"
-                                                                  :infourl "http://info.fi"}}})
-                                 handler
-                                 read-ok-body))]
+            (let [response (-> (request :put "/api/catalogue-items/edit")
+                               (authenticate +test-api-key+ "organization-owner1")
+                               (json-body {:id id
+                                           :organization {:organization/id changed-organization2}
+                                           :localizations {:sv {:title "Sv title 2"
+                                                                :infourl nil}
+                                                           :fi {:title "Fi title 2"
+                                                                :infourl "http://info.fi"}}})
+                               handler
+                               read-ok-body)]
               (is (:success response) (pr-str response))))
-
           (testing "... and edit (as organization owner but no rights to target org)"
-            (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                             (-> (request :put "/api/catalogue-items/edit")
-                                 (authenticate +test-api-key+ "organization-owner1")
-                                 (json-body {:id id
-                                             :organization {:organization/id "organization2"}
-                                             :localizations {:sv {:title "Sv title 2"
-                                                                  :infourl nil}
-                                                             :fi {:title "Fi title 2"
-                                                                  :infourl "http://info.fi"}}})
-                                 handler))]
+            (let [response (-> (request :put "/api/catalogue-items/edit")
+                               (authenticate +test-api-key+ "organization-owner1")
+                               (json-body {:id id
+                                           :organization {:organization/id "organization2"}
+                                           :localizations {:sv {:title "Sv title 2"
+                                                                :infourl nil}
+                                                           :fi {:title "Fi title 2"
+                                                                :infourl "http://info.fi"}}})
+                               handler)]
               (is (response-is-forbidden? response))
               (is (= "no access to organization \"organization2\"" (read-body response)))))
-
           (testing "... and edit (as organization owner for another organization)"
-            (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                             (-> (request :put "/api/catalogue-items/edit")
-                                 (authenticate +test-api-key+ "organization-owner2")
-                                 (json-body {:id id
-                                             :localizations {:sv {:title "Sv title 2"
-                                                                  :infourl nil}
-                                                             :fi {:title "Fi title 2"
-                                                                  :infourl "http://info.fi"}}})
-                                 handler))]
+            (let [response (-> (request :put "/api/catalogue-items/edit")
+                               (authenticate +test-api-key+ "organization-owner2")
+                               (json-body {:id id
+                                           :localizations {:sv {:title "Sv title 2"
+                                                                :infourl nil}
+                                                           :fi {:title "Fi title 2"
+                                                                :infourl "http://info.fi"}}})
+                               handler)]
               (is (response-is-forbidden? response))
               (is (= "no access to organization \"changed-organization2\"" (read-body response))))))))
 
     (testing "edit nonexisting"
-      (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                       (-> (request :put "/api/catalogue-items/edit")
-                           (authenticate +test-api-key+ owner)
-                           (json-body {:id 999999999
-                                       :localizations {:sv {:title "Sv title 2"
-                                                            :infourl nil}
-                                                       :fi {:title "Fi title"
-                                                            :infourl "http://info.fi"}}})
-                           handler))]
-        (is (response-is-not-found? response))))
-
-    (testing "edit one with no created applications"
-      (let [create (-> (request :post "/api/catalogue-items/create")
-                       (authenticate +test-api-key+ owner)
-                       (json-body {:form form-id
-                                   :resid res-id
-                                   :wfid wf-id
-                                   :organization {:organization/id "organization1"}
-                                   :localizations {:en {:title "No applications test"}}})
-                       handler
-                       read-ok-body)]
-        (is (:success create))
-        (let [response (is-logged? "rems.db.applications" :info "Reloading 0 applications because of catalogue item changes"
-                         (-> (request :put "/api/catalogue-items/edit")
-                             (authenticate +test-api-key+ owner)
-                             (json-body {:id (:id create)
-                                         :localizations {:en {:title "No applications test"
-                                                              :infourl "http://info.en"}}})
-                             handler
-                             read-ok-body))]
-          (is (:success response) (pr-str response)))))))
+      (let [response (-> (request :put "/api/catalogue-items/edit")
+                         (authenticate +test-api-key+ owner)
+                         (json-body {:id 999999999
+                                     :localizations {:sv {:title "Sv title 2"
+                                                          :infourl nil}
+                                                     :fi {:title "Fi title"
+                                                          :infourl "http://info.fi"}}})
+                         handler)]
+        (is (response-is-not-found? response))))))
 
 
 (deftest catalogue-items-api-security-test
@@ -315,24 +262,21 @@
                handler
                (read-body)))))
   (testing "edit without authentication"
-    (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                     (-> (request :post (str "/api/catalogue-items/edit"))
-                         (json-body {:id 1
-                                     :localizations {:en {:title "malicious localization"}}})
-                         handler))
+    (let [response (-> (request :post (str "/api/catalogue-items/edit"))
+                       (json-body {:id 1
+                                   :localizations {:en {: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"
-    (let [body (is-not-logged? "rems.db.applications" :info #"Reloading"
-                 (-> (request :put (str "/api/catalogue-items/edit"))
-                     (assoc-in [:headers "x-rems-api-key"] "invalid-api-key")
-                     (json-body {:id 1
-                                 :localizations {:en {:title "malicious localization"}}})
-                     handler
-                     (read-body)))]
-      (is (= "Invalid anti-forgery token"
-             body)))))
+    (is (= "Invalid anti-forgery token"
+           (-> (request :put (str "/api/catalogue-items/edit"))
+               (assoc-in [:headers "x-rems-api-key"] "invalid-api-key")
+               (json-body {:id 1
+                           :localizations {:en {:title "malicious localization"}}})
+               handler
+               (read-body))))))
 
 (deftest change-form-test ; XXX: deprecated
   (let [resource-id (test-helpers/create-resource! {:organization {:organization/id "organization1"}})
@@ -349,59 +293,52 @@
                                 :form-id old-form-id
                                 :categories [category]})]
 
-    (test-helpers/create-user! {:userid "alice"})
-    (test-helpers/create-application! {:catalogue-item-ids [old-catalogue-item-id]
-                                       :actor "alice"})
-
     (testing "when the form is changed a new catalogue item is created"
-      (let [new-catalogue-item-id (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                                    (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
-                                        (authenticate +test-api-key+ "owner")
-                                        (json-body {:form new-form-id})
-                                        handler
-                                        read-ok-body
-                                        :catalogue-item-id))]
-        (let [new-catalogue-item (-> (request :get (str "/api/catalogue-items/" new-catalogue-item-id))
-                                     (authenticate +test-api-key+ "owner")
-                                     handler
-                                     read-ok-body)
-              old-catalogue-item (-> (request :get (str "/api/catalogue-items/" old-catalogue-item-id))
-                                     (authenticate +test-api-key+ "owner")
-                                     handler
-                                     read-ok-body)]
-          (testing "the new item"
-            (is (:enabled new-catalogue-item))
-            (is (= new-form-id (:formid new-catalogue-item)) "has the new changed form id"))
-
-          (testing "the old item"
-            (is (:archived old-catalogue-item))
-            (is (not (:enabled old-catalogue-item))))
+      (let [new-catalogue-item-id (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
+                                      (authenticate +test-api-key+ "owner")
+                                      (json-body {:form new-form-id})
+                                      handler
+                                      read-ok-body
+                                      :catalogue-item-id)
+            new-catalogue-item (-> (request :get (str "/api/catalogue-items/" new-catalogue-item-id))
+                                   (authenticate +test-api-key+ "owner")
+                                   handler
+                                   read-ok-body)
+            old-catalogue-item (-> (request :get (str "/api/catalogue-items/" old-catalogue-item-id))
+                                   (authenticate +test-api-key+ "owner")
+                                   handler
+                                   read-ok-body)]
+        (testing "the new item"
+          (is (:enabled new-catalogue-item))
+          (is (= new-form-id (:formid new-catalogue-item)) "has the new changed form id"))
 
-          (let [same-keys [:wfid :workflow-name :resid :resource-id :resource-name :categories]]
-            (is (= (select-keys old-catalogue-item same-keys)
-                   (select-keys new-catalogue-item same-keys))))
+        (testing "the old item"
+          (is (:archived old-catalogue-item))
+          (is (not (:enabled old-catalogue-item))))
 
-          (doseq [langcode (into (keys (:localizations old-catalogue-item))
-                                 (keys (:localizations new-catalogue-item)))]
-            (is (= (dissoc (get-in old-catalogue-item [:localizations langcode]) :id)
-                   (dissoc (get-in new-catalogue-item [:localizations langcode]) :id)))))))
+        (let [same-keys [:wfid :workflow-name :resid :resource-id :resource-name :categories]]
+          (is (= (select-keys old-catalogue-item same-keys)
+                 (select-keys new-catalogue-item same-keys))))
+
+        (doseq [langcode (into (keys (:localizations old-catalogue-item))
+                               (keys (:localizations new-catalogue-item)))]
+          (is (= (dissoc (get-in old-catalogue-item [:localizations langcode]) :id)
+                 (dissoc (get-in new-catalogue-item [:localizations langcode]) :id))))))
 
     (testing "can change to form that's in another organization"
       (let [form-id (test-helpers/create-form! {:form/internal-name "wrong organization"
                                                 :organization {:organization/id "organization2"}})
-            response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                       (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
-                           (authenticate +test-api-key+ "owner")
-                           (json-body {:form form-id})
-                           handler
-                           read-ok-body))]
+            response (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
+                         (authenticate +test-api-key+ "owner")
+                         (json-body {:form form-id})
+                         handler
+                         read-ok-body)]
         (is (true? (:success response)))))
 
     (testing "can change to nil form"
-      (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                       (api-call :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form")
-                                 {:form nil}
-                                 +test-api-key+ "owner"))
+      (let [response (api-call :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form")
+                               {:form nil}
+                               +test-api-key+ "owner")
             new-catalogue-item-id (:catalogue-item-id response)]
         (is (true? (:success response)))
         (is (= {:id new-catalogue-item-id
@@ -412,10 +349,9 @@
                              +test-api-key+ "owner")
                    (select-keys [:formid :form-name :id :resource-id]))))
         (testing "and back"
-          (let [response (is-logged? "rems.db.applications" :info "Reloading 0 applications because of catalogue item changes"
-                           (api-call :post (str "/api/catalogue-items/" new-catalogue-item-id "/change-form")
-                                     {:form new-form-id}
-                                     +test-api-key+ "owner"))
+          (let [response (api-call :post (str "/api/catalogue-items/" new-catalogue-item-id "/change-form")
+                                   {:form new-form-id}
+                                   +test-api-key+ "owner")
                 new-new-catalogue-item-id (:catalogue-item-id response)]
             (is (true? (:success response)))
             (is (= {:id new-new-catalogue-item-id
@@ -427,20 +363,18 @@
                        (select-keys [:formid :form-name :id :resource-id]))))))))
 
     (testing "can change form as organization owner"
-      (let [body (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                   (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
-                       (authenticate +test-api-key+ "organization-owner1")
-                       (json-body {:form new-form-id})
-                       handler
-                       read-ok-body))]
-        (is (true? (:success body)))))
+      (is (true? (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
+                     (authenticate +test-api-key+ "organization-owner1")
+                     (json-body {:form new-form-id})
+                     handler
+                     read-ok-body
+                     :success))))
 
     (testing "can't change form as owner of different organization"
-      (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                       (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
-                           (authenticate +test-api-key+ "organization-owner2")
-                           (json-body {:form new-form-id})
-                           handler))]
+      (let [response (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/change-form"))
+                         (authenticate +test-api-key+ "organization-owner2")
+                         (json-body {:form new-form-id})
+                         handler)]
         (is (response-is-forbidden? response))
         (is (= "no access to organization \"organization1\"" (read-body response)))))))
 
@@ -465,19 +399,14 @@
                                 :wfid old-workflow-id
                                 :categories [category]})]
 
-    (test-helpers/create-user! {:userid "alice"})
-    (test-helpers/create-application! {:catalogue-item-ids [old-catalogue-item-id]
-                                       :actor "alice"})
-
     (testing "after update a new catalogue item is created"
-      (let [new-catalogue-item-id (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                                    (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
-                                        (authenticate +test-api-key+ "owner")
-                                        (json-body {:form new-form-id
-                                                    :workflow new-workflow-id})
-                                        handler
-                                        read-ok-body
-                                        :catalogue-item-id))
+      (let [new-catalogue-item-id (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
+                                      (authenticate +test-api-key+ "owner")
+                                      (json-body {:form new-form-id
+                                                  :workflow new-workflow-id})
+                                      handler
+                                      read-ok-body
+                                      :catalogue-item-id)
             new-catalogue-item (-> (request :get (str "/api/catalogue-items/" new-catalogue-item-id))
                                    (authenticate +test-api-key+ "owner")
                                    handler
@@ -510,24 +439,22 @@
                    (dissoc (get-in new-catalogue-item [:localizations langcode]) :id)))))
 
         (testing "and if we are done already, we get the same item"
-          (let [new-catalogue-item-id2 (is-not-logged? "rems.db.applications" :info #"Reloading"
-                                         (-> (request :post (str "/api/catalogue-items/" new-catalogue-item-id "/update"))
-                                             (authenticate +test-api-key+ "owner")
-                                             (json-body {:form new-form-id
-                                                         :workflow new-workflow-id})
-                                             handler
-                                             read-ok-body
-                                             :catalogue-item-id))]
+          (let [new-catalogue-item-id2 (-> (request :post (str "/api/catalogue-items/" new-catalogue-item-id "/update"))
+                                           (authenticate +test-api-key+ "owner")
+                                           (json-body {:form new-form-id
+                                                       :workflow new-workflow-id})
+                                           handler
+                                           read-ok-body
+                                           :catalogue-item-id)]
             (is (= new-catalogue-item-id new-catalogue-item-id2))))))
 
     (testing "when the form is updated a new catalogue item is created"
-      (let [new-catalogue-item-id (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                                    (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
-                                        (authenticate +test-api-key+ "owner")
-                                        (json-body {:form new-form-id})
-                                        handler
-                                        read-ok-body
-                                        :catalogue-item-id))
+      (let [new-catalogue-item-id (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
+                                      (authenticate +test-api-key+ "owner")
+                                      (json-body {:form new-form-id})
+                                      handler
+                                      read-ok-body
+                                      :catalogue-item-id)
             new-catalogue-item (-> (request :get (str "/api/catalogue-items/" new-catalogue-item-id))
                                    (authenticate +test-api-key+ "owner")
                                    handler
@@ -546,13 +473,12 @@
                    (select-keys new-catalogue-item same-keys)))))))
 
     (testing "when the workflow is updated a new catalogue item is created"
-      (let [new-catalogue-item-id (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                                    (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
-                                        (authenticate +test-api-key+ "owner")
-                                        (json-body {:workflow new-workflow-id})
-                                        handler
-                                        read-ok-body
-                                        :catalogue-item-id))
+      (let [new-catalogue-item-id (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
+                                      (authenticate +test-api-key+ "owner")
+                                      (json-body {:workflow new-workflow-id})
+                                      handler
+                                      read-ok-body
+                                      :catalogue-item-id)
             new-catalogue-item (-> (request :get (str "/api/catalogue-items/" new-catalogue-item-id))
                                    (authenticate +test-api-key+ "owner")
                                    handler
@@ -575,20 +501,18 @@
                                                 :organization {:organization/id "organization2"}})
             workflow-id (test-helpers/create-workflow! {:title "wrong organization"
                                                         :organization {:organization/id "organization2"}})
-            response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                       (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
-                           (authenticate +test-api-key+ "owner")
-                           (json-body {:form form-id
-                                       :workflow workflow-id})
-                           handler
-                           read-ok-body))]
+            response (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
+                         (authenticate +test-api-key+ "owner")
+                         (json-body {:form form-id
+                                     :workflow workflow-id})
+                         handler
+                         read-ok-body)]
         (is (true? (:success response)))))
 
     (testing "can update to nil form"
-      (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                       (api-call :post (str "/api/catalogue-items/" old-catalogue-item-id "/update")
-                                 {:form nil}
-                                 +test-api-key+ "owner"))
+      (let [response (api-call :post (str "/api/catalogue-items/" old-catalogue-item-id "/update")
+                               {:form nil}
+                               +test-api-key+ "owner")
             new-catalogue-item-id (:catalogue-item-id response)]
         (is (true? (:success response)))
         (is (= {:id new-catalogue-item-id
@@ -599,10 +523,9 @@
                              +test-api-key+ "owner")
                    (select-keys [:formid :form-name :id :resource-id]))))
         (testing "and back"
-          (let [response (is-logged? "rems.db.applications" :info "Reloading 0 applications because of catalogue item changes"
-                           (api-call :post (str "/api/catalogue-items/" new-catalogue-item-id "/update")
-                                     {:form new-form-id}
-                                     +test-api-key+ "owner"))
+          (let [response (api-call :post (str "/api/catalogue-items/" new-catalogue-item-id "/update")
+                                   {:form new-form-id}
+                                   +test-api-key+ "owner")
                 new-new-catalogue-item-id (:catalogue-item-id response)]
             (is (true? (:success response)))
             (is (= {:id new-new-catalogue-item-id
@@ -614,22 +537,20 @@
                        (select-keys [:formid :form-name :id :resource-id]))))))))
 
     (testing "can update as organization owner"
-      (let [body (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                   (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
-                       (authenticate +test-api-key+ "organization-owner1")
-                       (json-body {:form new-form-id
-                                   :workflow new-workflow-id})
-                       handler
-                       read-ok-body))]
-        (is (true? (:success body)))))
+      (is (true? (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
+                     (authenticate +test-api-key+ "organization-owner1")
+                     (json-body {:form new-form-id
+                                 :workflow new-workflow-id})
+                     handler
+                     read-ok-body
+                     :success))))
 
     (testing "can't update as owner of different organization"
-      (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                       (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
-                           (authenticate +test-api-key+ "organization-owner2")
-                           (json-body {:form new-form-id
-                                       :workflow new-workflow-id})
-                           handler))]
+      (let [response (-> (request :post (str "/api/catalogue-items/" old-catalogue-item-id "/update"))
+                         (authenticate +test-api-key+ "organization-owner2")
+                         (json-body {:form new-form-id
+                                     :workflow new-workflow-id})
+                         handler)]
         (is (response-is-forbidden? response))
         (is (= "no access to organization \"organization1\"" (read-body response)))))))
 
@@ -643,58 +564,42 @@
         fetch #(api-call :get (str "/api/catalogue-items/" id)
                          nil
                          +test-api-key+ "owner")]
-
-    (test-helpers/create-user! {:userid "alice"})
-    (test-helpers/create-application! {:catalogue-item-ids [id]
-                                       :actor "alice"})
-
     (doseq [user ["owner" "organization-owner1"]]
       (testing user
         (testing "disable"
-          (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                           (api-call :put "/api/catalogue-items/enabled"
-                                     {:id id
-                                      :enabled false}
-                                     +test-api-key+ user))]
-            (is (:success response))
-            (is (false? (:enabled (fetch))))))
+          (is (:success (api-call :put "/api/catalogue-items/enabled"
+                                  {:id id
+                                   :enabled false}
+                                  +test-api-key+ user)))
+          (is (false? (:enabled (fetch)))))
         (testing "enable"
-          (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                           (api-call :put "/api/catalogue-items/enabled"
-                                     {:id id
-                                      :enabled true}
-                                     +test-api-key+ user))]
-            (is (:success response))
-            (is (true? (:enabled (fetch))))))
+          (is (:success (api-call :put "/api/catalogue-items/enabled"
+                                  {:id id
+                                   :enabled true}
+                                  +test-api-key+ user)))
+          (is (true? (:enabled (fetch)))))
         (testing "archive"
-          (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                           (api-call :put "/api/catalogue-items/archived"
-                                     {:id id
-                                      :archived true}
-                                     +test-api-key+ user))]
-            (is (:success response))
-            (is (true? (:archived (fetch))))))
+          (is (:success (api-call :put "/api/catalogue-items/archived"
+                                  {:id id
+                                   :archived true}
+                                  +test-api-key+ user)))
+          (is (true? (:archived (fetch)))))
         (testing "unarchive"
-          (let [response (is-logged? "rems.db.applications" :info "Reloading 1 applications because of catalogue item changes"
-                           (api-call :put "/api/catalogue-items/archived"
-                                     {:id id
-                                      :archived false}
-                                     +test-api-key+ user))]
-            (is (:success response))
-            (is (false? (:archived (fetch))))))))
-
+          (is (:success (api-call :put "/api/catalogue-items/archived"
+                                  {:id id
+                                   :archived false}
+                                  +test-api-key+ user)))
+          (is (false? (:archived (fetch)))))))
     (testing "incorrect organization owner can't"
       (testing "enable"
-        (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                         (-> (request :put "/api/catalogue-items/enabled")
-                             (authenticate +test-api-key+ "organization-owner2")
-                             (json-body {:id id :enabled true})
-                             handler))]
+        (let [response (-> (request :put "/api/catalogue-items/enabled")
+                           (authenticate +test-api-key+ "organization-owner2")
+                           (json-body {:id id :enabled true})
+                           handler)]
           (is (response-is-forbidden? response))))
       (testing "archive"
-        (let [response (is-not-logged? "rems.db.applications" :info #"Reloading"
-                         (-> (request :put "/api/catalogue-items/archived")
-                             (authenticate +test-api-key+ "organization-owner2")
-                             (json-body {:id id :archived true})
-                             handler))]
+        (let [response (-> (request :put "/api/catalogue-items/archived")
+                           (authenticate +test-api-key+ "organization-owner2")
+                           (json-body {:id id :archived true})
+                           handler)]
           (is (response-is-forbidden? response)))))))
diff --git a/test/clj/rems/application/test_model.clj b/test/clj/rems/application/test_model.clj
index c1f5a03db..54a699b5e 100644
--- a/test/clj/rems/application/test_model.clj
+++ b/test/clj/rems/application/test_model.clj
@@ -1302,7 +1302,7 @@
                                 :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler" :secret "secret"}
                                 :event/visibility :visibility/public}]
           :application/user-roles {"handler" #{:handler}, "reporter1" #{:reporter}}
-          :application/role-permissions {}
+          :application/role-permissions nil
           :application/description "foo"
           :application/forms [{:form/id 40
                                :form/title "form name" ; deprecated
@@ -1381,7 +1381,7 @@
                                 :application/id 1
                                 :application/resources [{:catalogue-item/id 10 :resource/ext-id "urn:11"}
                                                         {:catalogue-item/id 20 :resource/ext-id "urn:21"}]}
-                               get-user get-catalogue-item get-resource get-config))))
+                               get-user get-catalogue-item get-resource))))
   (testing "decision-requested"
     (is (= {:event/type :application.event/decision-requested
             :event/time (DateTime. 1)
@@ -1396,7 +1396,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/deciders ["decider" "reviewer"]}
-                               get-user get-catalogue-item get-resource get-config))))
+                               get-user get-catalogue-item get-resource))))
   (testing "review-requested"
     (is (= {:event/type :application.event/review-requested
             :event/time (DateTime. 1)
@@ -1411,7 +1411,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/reviewers ["decider" "reviewer"]}
-                               get-user get-catalogue-item get-resource get-config))))
+                               get-user get-catalogue-item get-resource))))
   (testing "member-added"
     (is (= {:event/type :application.event/member-added
             :event/time (DateTime. 1)
@@ -1425,7 +1425,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/member {:userid "member"}}
-                               get-user get-catalogue-item get-resource get-config))))
+                               get-user get-catalogue-item get-resource))))
 
   (testing "member-removed"
     (is (= {:event/type :application.event/member-removed
@@ -1440,7 +1440,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/member {:userid "member"}}
-                               get-user get-catalogue-item get-resource get-config)))))
+                               get-user get-catalogue-item get-resource)))))
 
 (deftest test-enrich-answers
   (testing "draft"
@@ -1501,7 +1501,7 @@
                                                                {:userid "edward"
                                                                 :name "Edward Elric"
                                                                 :handler/active? true}]}}
-           (-> (model/enrich-workflow-handlers application #(model/get-workflow-handlers % get-workflow get-user))
+           (-> (model/enrich-workflow-handlers application get-workflow get-user)
                (select-keys [:application/workflow]))))))
 
 (deftest test-enrich-deadline
diff --git a/test/clj/rems/email/test_template.clj b/test/clj/rems/email/test_template.clj
index b3508412e..a4046a38e 100644
--- a/test/clj/rems/email/test_template.clj
+++ b/test/clj/rems/email/test_template.clj
@@ -124,7 +124,7 @@
                                                              (assert (string? userid))
                                                              {:language lang})]
        (sort-emails (template/event-to-emails
-                     (model/enrich-event event get-user get-catalogue-item get-resource get-config)
+                     (model/enrich-event event get-user get-catalogue-item get-resource)
                      application)))))
   ([base-events event]
    (emails :en base-events event)))
diff --git a/test/clj/rems/test_cache.clj b/test/clj/rems/test_cache.clj
index 1740c056d..fecb75aa2 100644
--- a/test/clj/rems/test_cache.clj
+++ b/test/clj/rems/test_cache.clj
@@ -7,9 +7,8 @@
             [medley.core :refer [map-vals]]
             [rems.cache :as cache]
             [rems.common.dependency :as dep]
-            [rems.common.util :refer [range-1]]
-            [rems.concurrency :as concurrency]
-            [rems.config]))
+            [rems.common.util :refer [getx range-1]]
+            [rems.concurrency :as concurrency]))
 
 (defn- submit-all [thread-pool & fns]
   (->> fns
@@ -34,8 +33,7 @@
 
 (deftest test-basic-cache
   (with-redefs [rems.cache/caches (doto caches (reset! nil))
-                rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))
-                rems.config/env {:dev true}]
+                rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))]
     (let [c (cache/basic {:id ::test-cache
                           :miss-fn (fn [id] (miss-fn id))
                           :reload-fn (fn [] (reload-fn))})]
@@ -128,33 +126,21 @@
 (deftest test-cache-dependencies
   (testing "cannot create caches with circular dependencies"
     (with-redefs [rems.cache/caches (doto caches (reset! nil))
-                  rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))
-                  rems.config/env {:dev true}]
+                  rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))]
       (let [_cache-a (cache/basic {:id :a :depends-on [:b]})]
         (is (thrown-with-msg? RuntimeException #"Circular dependency between :b and :a"
                               (cache/basic {:id :b :depends-on [:a]}))))))
 
-  (testing "existing cache id"
+  (testing "cannot override existing cache id"
     (with-redefs [rems.cache/caches (doto caches (reset! nil))
                   rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))]
-
-      (testing "only warning is logged in dev"
-        (with-redefs [rems.config/env {:dev true}]
-          (log-test/with-log
-            (cache/basic {:id :a})
-            (is (not (log-test/logged? "rems.cache" :warn "overriding cache id :a")))
-            (cache/basic {:id :a})
-            (is (log-test/logged? "rems.cache" :warn "overriding cache id :a")))))
-
-      (testing "assertion error when not in dev"
-        (with-redefs [rems.config/env {}]
-          (is (thrown-with-msg? AssertionError #"Assert failed: error overriding cache id :a"
-                                (cache/basic {:id :a})))))))
+      (let [_cache-a (cache/basic {:id :a})]
+        (is (thrown-with-msg? AssertionError #"Assert failed: error overriding cache id :a"
+                              (cache/basic {:id :a}))))))
 
   (testing "can create basic caches with dependencies"
     (with-redefs [rems.cache/caches (doto caches (reset! nil))
-                  rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))
-                  rems.config/env {:dev true}]
+                  rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))]
       (let [cache-a (cache/basic {:id :a
                                   :reload-fn (fn [] {1 true})})
             cache-b (cache/basic {:id :b
@@ -261,8 +247,7 @@
   ;; - then check that each cache saw the atom values in monotonically increasing order
 
   (with-redefs [rems.cache/caches (doto caches (reset! nil))
-                rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))
-                rems.config/env {:dev true}]
+                rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))]
     (let [progress (atom 0)
           current-a (atom 1)
           current-b (atom 1)
@@ -462,8 +447,7 @@
   ;; - print observed waiting times
   ;; - check end was reached
   (with-redefs [rems.cache/caches (doto caches (reset! nil))
-                rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))
-                rems.config/env {:dev true}]
+                rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph)))]
     (let [current-a (atom 1)
           current-b (atom 1)
 
diff --git a/test/clj/rems/test_layers.clj b/test/clj/rems/test_layers.clj
index a7f4685be..a8752601a 100644
--- a/test/clj/rems/test_layers.clj
+++ b/test/clj/rems/test_layers.clj
@@ -210,17 +210,15 @@
                            :when (> sum-time hide-cutoff-ms)]
                        [(sym->id from) (sym->id to) {;;:color (if (and avg (< avg cutoff-ms)) "#gray" "black")
                                                      :weight (or (and min-time (number? min-time)) 0.01)
-                                                     :penwidth (min 30
-                                                                    ;;(max (or (and min-time (number? min-time) (Math/sqrt min-time)) 0.1)
-                                                                    (max (or (and sum-time (number? sum-time) (* 4.0 (- (Math/log sum-time) 1))) 0.1)
-                                                                         0.1))
+                                                     :penwidth (max (or (and min-time (number? min-time) (Math/sqrt min-time)) 0.1)
+                                                                    0.1)
                                                      :label (if (and max-time (< max-time cutoff-ms))
                                                               ""
                                                               [:table {:border 0}
                                                                (when (> c 1) [:tr [:td "count"] [:td c]])
-                                                               (when (>= c 1) [:tr [:td "sum"] [:td (format "%.2f ms" sum-time)]])
+                                                               (when (> c 1) [:tr [:td "sum"] [:td (format "%.2f ms" sum-time)]])
                                                                (when (> c 1) [:tr [:td "min"] [:td (format "%.2f ms" min-time)]])
-                                                               (when (> c 1) [:tr [:td (if (> c 1) "avg" "")] [:td (format "%.2f ms" avg)]])
+                                                               [:tr [:td (if (> c 1) "avg" "")] [:td (format "%.2f ms" avg)]]
                                                                (when (> c 1) [:tr [:td "max"] [:td (format "%.2f ms" max-time)]])
                                                                (cond (= lazy c) [:tr [:td "lazy"] [:td "true"]]
                                                                      (pos? lazy) [:tr [:td "lazy"] [:td (format "%d/%d (%.0f%%)" (int lazy) (int c) (* 100.0 (/ lazy (double c))))]])])}]))
diff --git a/test/clj/rems/test_performance.clj b/test/clj/rems/test_performance.clj
index fd7bc68d2..946ef856e 100644
--- a/test/clj/rems/test_performance.clj
+++ b/test/clj/rems/test_performance.clj
@@ -117,7 +117,7 @@
                      :event/type :application.event/decided
                      :event/actor (:decider +fake-users+)
                      :application/decision :approved}
-                    (rems.application.model/enrich-event (into +fake-user-data+ fake-handlers) nil nil (fn [] {})))]
+                    (rems.application.model/enrich-event (into +fake-user-data+ fake-handlers) nil nil))]
     {:application application
      :event decided}))