Repository: CSCfi/rems
Pull request #1489 (2019-08-14 11:47:07)

circleci: attempt workaround for builds failing in apt


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e7688cc81..40a44ae4e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -59,6 +59,8 @@ jobs:
       - restore_cache:
           keys:
           - v2-dependencies-{{ checksum "project.clj" }}
+      # workaround for apt failing with Could not get lock /var/lib/apt/lists/lock
+      - run: sudo systemctl kill --kill-who=all apt-daily.service
       - run: sudo apt remove google-chrome-stable
       - run: sudo rm /usr/local/bin/chromedriver
       - run: sudo apt update
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7b16fb43..507b24fe1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,7 +34,6 @@ Changes since v2.4
 - Store user language preference, use chosen language for emails
 - Upgraded swagger-ui from 2 to 3
 - Extra pages (#472)
-- Full-text search for all application content (#873)
 
 ### Enhancements
 - Improved version information in footer
diff --git a/docs/release.md b/docs/release.md
index 48c09e907..514d742d9 100644
--- a/docs/release.md
+++ b/docs/release.md
@@ -1,19 +1,18 @@
 # Releases and releasing
 
 ## Naming
-Releases are named by streets starting from CSC headquarters in Keilaniemi.
+Releases are named by streets starting from CSC headquarters in Keilaniemi. 
 
-I.e. Keilaranta (v2.0), Otaniementie (v2.1), Vuorimiehentie (v2.2.), Tekniikantie (v2.3) to Tietotie (v2.4).
+I.e. Keilaranta (v2.0), Otaniementie (v2.1), Vuorimiehentie (v2.2.), Tekniikantie (v2.3) to Tietotie (v2.4). 
 The destination to aim for is up to you.
 
 See [Release page](https://github.com/CSCfi/rems/releases).
 
 ## Release notes
 
-Release notes can be found in the [changelog](../CHANGELOG.md). The
-changelog is updated by developers via the normal pull request process.
+Release notes are hand-written by going through all the issues that are included in the release.
 
-Releases can be found on the github [release page](https://github.com/CSCfi/rems/releases).
+See [Release page](https://github.com/CSCfi/rems/releases).
 
 ## Environments
 
diff --git a/resources/translations/en.edn b/resources/translations/en.edn
index 8c13385ee..96e520a09 100644
--- a/resources/translations/en.edn
+++ b/resources/translations/en.edn
@@ -302,8 +302,9 @@
            :phase-pending "Phase pending:"
            :phase-rejected "Phase rejected:"
            :rejected "Rejected"}
-  :search {:clear-search "Clear search"
-           :search "Search"}
+  :search {:close-search "Close search"
+           :open-search "Open search"
+           :search-parameters "Search parameters"}
   :table {:ascending-order "Ascending order"
           :descending-order "Descending order"}
   :unauthorized-page {:unauthorized "Unauthorized"
diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn
index 265f03700..e9ad3ea7c 100644
--- a/resources/translations/fi.edn
+++ b/resources/translations/fi.edn
@@ -300,8 +300,9 @@
            :phase-pending "Vaihe tulossa:"
            :phase-rejected "Vaihe hylätty:"
            :rejected "Hylätty"}
-  :search {:clear-search "Tyhjennä hakuehdot"
-           :search "Etsi"} ; "Hae" would be more natural, but the catalogue page uses the same word for another meaning
+  :search {:close-search "Sulje haku"
+           :open-search "Avaa haku"
+           :search-parameters "Hakutermit"}
   :table {:ascending-order "Nouseva järjestys"
           :descending-order "Laskeva järjestys"}
   :unauthorized-page {:unauthorized "Ei käyttöoikeutta"
diff --git a/src/clj/rems/api/licenses.clj b/src/clj/rems/api/licenses.clj
index c8152fa43..1fde96f60 100644
--- a/src/clj/rems/api/licenses.clj
+++ b/src/clj/rems/api/licenses.clj
@@ -4,7 +4,7 @@
             [rems.api.util :as api-util] ; required for route :roles
             [rems.db.attachments :as attachments]
             [rems.db.core :as db]
-            [rems.api.services.licenses :as licenses]
+            [rems.db.licenses :as licenses]
             [rems.util :refer [getx-user-id]]
             [ring.middleware.multipart-params :as multipart]
             [ring.swagger.upload :as upload]
diff --git a/src/clj/rems/api/services/licenses.clj b/src/clj/rems/api/services/licenses.clj
deleted file mode 100644
index d75dc7b8f..000000000
--- a/src/clj/rems/api/services/licenses.clj
+++ /dev/null
@@ -1,79 +0,0 @@
-(ns rems.api.services.licenses
-  "Serving licenses for API."
-  (:require [rems.common-util :refer [distinct-by]]
-            [rems.db.core :as db]
-            [rems.db.licenses :as licenses]
-            [rems.db.resource :as resource]
-            [rems.db.workflow :as workflow])
-  (:import (java.io FileInputStream ByteArrayOutputStream)))
-
-(defn create-license! [{:keys [title licensetype textcontent localizations attachment-id]} user-id]
-  (let [license (db/create-license! {:owneruserid user-id
-                                     :modifieruserid user-id
-                                     :type licensetype
-                                     :title title
-                                     :textcontent textcontent
-                                     :attachmentId attachment-id})
-        licid (:id license)]
-    (doseq [[langcode localization] localizations]
-      (db/create-license-localization! {:licid licid
-                                        :langcode (name langcode)
-                                        :title (:title localization)
-                                        :textcontent (:textcontent localization)
-                                        :attachmentId (:attachment-id localization)}))
-    {:success (not (nil? licid))
-     :id licid}))
-
-(defn create-license-attachment! [{:keys [tempfile filename content-type]} user-id]
-  (let [byte-array (with-open [input (FileInputStream. tempfile)
-                               buffer (ByteArrayOutputStream.)]
-                     (clojure.java.io/copy input buffer)
-                     (.toByteArray buffer))]
-    (select-keys
-     (db/create-license-attachment! {:user user-id
-                                     :filename filename
-                                     :type content-type
-                                     :data byte-array})
-     [:id])))
-
-(defn remove-license-attachment!
-  [attachment-id]
-  (db/remove-license-attachment! {:id attachment-id}))
-
-(defn- get-license-usage [id]
-  ;; these could be db joins
-  (let [resources (->> (db/get-resources-for-license {:id id})
-                       (map :resid)
-                       (map resource/get-resource)
-                       (remove :archived)
-                       (map #(select-keys % [:id :resid])))
-        workflows (->> (db/get-workflows-for-license {:id id})
-                       (map :wfid)
-                       (map workflow/get-workflow)
-                       (remove :archived)
-                       (map #(select-keys % [:id :title])))]
-    (when (or (seq resources) (seq workflows))
-      {:resources resources
-       :workflows workflows})))
-
-(defn update-license! [command]
-  (let [usage (get-license-usage (:id command))]
-    (if (and (:archived command) usage)
-      {:success false
-       :errors [(merge {:type :t.administration.errors/license-in-use}
-                       usage)]}
-      (do
-        (db/set-license-state! command)
-        {:success true}))))
-
-(defn get-license
-  "Get a single license by id"
-  [id]
-  (licenses/get-license id))
-
-(defn get-all-licenses
-  "Get all licenses.
-
-   filters is a map of key-value pairs that must be present in the licenses"
-  [filters]
-  (licenses/get-all-licenses filters))
diff --git a/src/clj/rems/application/search.clj b/src/clj/rems/application/search.clj
index 54e7c88bd..6e55bba0a 100644
--- a/src/clj/rems/application/search.clj
+++ b/src/clj/rems/application/search.clj
@@ -1,25 +1,19 @@
 (ns rems.application.search
   (:require [clojure.java.io :as io]
-            [clojure.string :as str]
             [clojure.tools.logging :as log]
             [mount.core :as mount]
-            [rems.application-util :as application-util]
             [rems.config :refer [env]]
             [rems.db.applications :as applications]
-            [rems.db.events :as events]
-            [rems.text :as text])
-  (:import [org.apache.lucene.analysis Analyzer]
-           [org.apache.lucene.analysis.standard StandardAnalyzer]
+            [rems.db.events :as events])
+  (:import [org.apache.lucene.analysis.standard StandardAnalyzer]
            [org.apache.lucene.document Document StringField Field$Store TextField]
-           [org.apache.lucene.index IndexWriter IndexWriterConfig IndexWriterConfig$OpenMode Term]
+           [org.apache.lucene.index IndexWriter IndexWriterConfig IndexWriterConfig$OpenMode]
            [org.apache.lucene.queryparser.flexible.core QueryNodeException]
            [org.apache.lucene.queryparser.flexible.standard StandardQueryParser]
            [org.apache.lucene.search IndexSearcher ScoreDoc TopDocs SearcherManager SearcherFactory Query]
            [org.apache.lucene.store Directory NIOFSDirectory]))
 
-(def ^:private ^Analyzer analyzer (StandardAnalyzer.))
-
-(def ^:private ^String app-id-field "app-id")
+(def ^:private analyzer (StandardAnalyzer.))
 
 (mount/defstate ^Directory search-index
   :start (let [directory (NIOFSDirectory. (.toPath (io/file (:search-index-path env))))]
@@ -33,48 +27,12 @@
           (.close ^SearcherManager (::searcher-manager @search-index))
           (.close ^Directory (::directory @search-index))))
 
-(defn- index-terms-for-application [app]
-  {:id (->> [(:application/id app)
-             (:application/external-id app)]
-            (str/join " "))
-   :applicant (->> [(:application/applicant app)
-                    (application-util/get-applicant-name app)
-                    (:mail (:application/applicant-attributes app))]
-                   (str/join " "))
-   :member (->> (:application/members app)
-                (mapcat (fn [member]
-                          [(:userid member)
-                           (application-util/get-member-name member)
-                           (:mail member)]))
-                (str/join " "))
-   :title (:application/description app)
-   :resource (->> (:application/resources app)
-                  (mapcat (fn [resource]
-                            (vals (:catalogue-item/title resource))))
-                  (str/join " "))
-   :state (->> (:languages env)
-               (map (fn [lang]
-                      (text/with-language lang
-                        #(text/localize-state (:application/state app)))))
-               (str/join " "))
-   :form (->> (:form/fields (:application/form app))
-              (map (fn [field]
-                     ;; TODO: filter out checkboxes, attachments etc?
-                     (:field/value field)))
-              (str/join " "))})
-
 (defn- index-application! [^IndexWriter writer app]
   (log/info "Indexing application" (:application/id app))
-  (let [doc (Document.)
-        app-id (str (:application/id app))
-        terms (index-terms-for-application app)]
-    ;; metadata
-    (.add doc (StringField. app-id-field app-id Field$Store/YES))
-    ;; searchable fields
-    (doseq [[k v] terms]
-      (.add doc (TextField. (name k) v Field$Store/NO)))
-    (.add doc (TextField. "all" (str/join " " (vals (into (sorted-map) terms))) Field$Store/NO))
-    (.updateDocument writer (Term. app-id-field app-id) doc)))
+  (let [doc (Document.)]
+    (.add doc (StringField. "id" (str (:application/id app)) Field$Store/YES))
+    (.add doc (TextField. "applicant" (str (:application/applicant app)) Field$Store/NO))
+    (.addDocument writer doc)))
 
 (def ^:private refresh-lock (Object.))
 
@@ -103,13 +61,13 @@
 (defn- get-application-ids [^IndexSearcher searcher ^TopDocs results]
   (doall (for [^ScoreDoc hit (.-scoreDocs results)]
            (let [doc (.doc searcher (.-doc hit))
-                 app-id (.get doc app-id-field)]
-             (Long/parseLong app-id)))))
+                 id (.get doc "id")]
+             (Long/parseLong id)))))
 
 (defn- ^Query parse-query [^String query]
   (try
     (-> (StandardQueryParser. analyzer)
-        (.parse query "all"))
+        (.parse query "applicant")) ; TODO: change defaultField to full text search
     (catch QueryNodeException e
       (log/info (str "Failed to parse query '" query "', " e))
       nil)))
diff --git a/src/clj/rems/db/licenses.clj b/src/clj/rems/db/licenses.clj
index 45bb245eb..13d917f88 100644
--- a/src/clj/rems/db/licenses.clj
+++ b/src/clj/rems/db/licenses.clj
@@ -1,7 +1,9 @@
 (ns rems.db.licenses
   "querying localized licenses"
   (:require [rems.common-util :refer [distinct-by]]
-            [rems.db.core :as db])
+            [rems.db.core :as db]
+            [rems.db.resource :as resource]
+            [rems.db.workflow :as workflow])
   (:import (java.io FileInputStream ByteArrayOutputStream)))
 
 (defn- format-license [license]
@@ -82,3 +84,62 @@
        (map (partial db/assoc-expired now))
        (remove :expired)
        (distinct-by :id)))
+
+(defn create-license! [{:keys [title licensetype textcontent localizations attachment-id]} user-id]
+  (let [license (db/create-license! {:owneruserid user-id
+                                     :modifieruserid user-id
+                                     :type licensetype
+                                     :title title
+                                     :textcontent textcontent
+                                     :attachmentId attachment-id})
+        licid (:id license)]
+    (doseq [[langcode localization] localizations]
+      (db/create-license-localization! {:licid licid
+                                        :langcode (name langcode)
+                                        :title (:title localization)
+                                        :textcontent (:textcontent localization)
+                                        :attachmentId (:attachment-id localization)}))
+    {:success (not (nil? licid))
+     :id licid}))
+
+(defn create-license-attachment! [{:keys [tempfile filename content-type]} user-id]
+  (let [byte-array (with-open [input (FileInputStream. tempfile)
+                               buffer (ByteArrayOutputStream.)]
+                     (clojure.java.io/copy input buffer)
+                     (.toByteArray buffer))]
+    (select-keys
+     (db/create-license-attachment! {:user user-id
+                                     :filename filename
+                                     :type content-type
+                                     :data byte-array})
+     [:id])))
+
+(defn remove-license-attachment!
+  [attachment-id]
+  (db/remove-license-attachment! {:id attachment-id}))
+
+(defn- get-license-usage [id]
+  ;; these could be db joins
+  (let [resources (->> (db/get-resources-for-license {:id id})
+                       (map :resid)
+                       (map resource/get-resource)
+                       (remove :archived)
+                       (map #(select-keys % [:id :resid])))
+        workflows (->> (db/get-workflows-for-license {:id id})
+                       (map :wfid)
+                       (map workflow/get-workflow)
+                       (remove :archived)
+                       (map #(select-keys % [:id :title])))]
+    (when (or (seq resources) (seq workflows))
+      {:resources resources
+       :workflows workflows})))
+
+(defn update-license! [command]
+  (let [usage (get-license-usage (:id command))]
+    (if (and (:archived command) usage)
+      {:success false
+       :errors [(merge {:type :t.administration.errors/license-in-use}
+                       usage)]}
+      (do
+        (db/set-license-state! command)
+        {:success true}))))
diff --git a/src/clj/rems/locales.clj b/src/clj/rems/locales.clj
index 3e420047a..953e67d64 100644
--- a/src/clj/rems/locales.clj
+++ b/src/clj/rems/locales.clj
@@ -16,9 +16,9 @@
                         (and file (.exists file)) file
                         resource resource
                         :else (throw (FileNotFoundException.
-                                      (if file
-                                        (str "translations could not be found in " file " file or " resource-path " resource")
-                                        (str "translations could not be found in " resource-path " resource and " :translations-directory " was not set")))))]
+                                       (if file
+                                         (str "translations could not be found in " file " file or " resource-path " resource")
+                                         (str "translations could not be found in " resource-path " resource and " :translations-directory " was not set")))))]
     (read-string (slurp file-contents))))
 
 (defn- extra-translations-path [theme-path]
@@ -43,5 +43,4 @@
 (defstate translations :start (load-translations env))
 
 (defn tempura-config []
-  (assert (map? translations) {:translations translations})
   {:dict translations})
diff --git a/src/clj/rems/poller/email.clj b/src/clj/rems/poller/email.clj
index eb8a901e6..335edb2ff 100644
--- a/src/clj/rems/poller/email.clj
+++ b/src/clj/rems/poller/email.clj
@@ -71,15 +71,13 @@
                                 (:application/applicant application)
                                 (resources-for-email application)
                                 (link-to-application (:application/id event)))
-          :body (str
-                 (text-format body-text
-                              recipient
-                              (:event/actor event)
-                              (application-id-for-email application)
-                              (:application/applicant application)
-                              (resources-for-email application)
-                              (link-to-application (:application/id event)))
-                 (text :t.email/footer))})))))
+          :body (text-format body-text
+                             recipient
+                             (:event/actor event)
+                             (application-id-for-email application)
+                             (:application/applicant application)
+                             (resources-for-email application)
+                             (link-to-application (:application/id event)))})))))
 
 (defmethod event-to-emails-impl :application.event/approved [event application]
   (concat (emails-to-recipients (applicant-and-members application)
@@ -175,11 +173,9 @@
         :subject (text-format :t.email.member-invited/subject
                               (:email (:application/member event))
                               (invitation-link (:invitation/token event)))
-        :body (str
-               (text-format :t.email.member-invited/message
-                            (:email (:application/member event))
-                            (invitation-link (:invitation/token event)))
-               (text :t.email/footer))}])))
+        :body (text-format :t.email.member-invited/message
+                           (:email (:application/member event))
+                           (invitation-link (:invitation/token event)))}])))
 
 ;; TODO member-joined?
 
@@ -211,7 +207,8 @@
       (log/info "pretending to send email:" (pr-str email-spec))
       (let [email (assoc email-spec
                          :from (:mail-from env)
-                         :body (:body email-spec)
+                         :body (str (:body email-spec)
+                                    (text :t.email/footer))
                          :to (or (:to email-spec)
                                  (util/get-user-mail
                                   (users/get-user-attributes
diff --git a/src/cljs/rems/actions.cljs b/src/cljs/rems/actions.cljs
index fd1f05906..28a726403 100644
--- a/src/cljs/rems/actions.cljs
+++ b/src/cljs/rems/actions.cljs
@@ -1,24 +1,114 @@
 (ns rems.actions
   "The /actions page that shows a list of applications you can act on."
   (:require [re-frame.core :as rf]
+            [reagent.core :as r]
             [rems.application-list :as application-list]
-            [rems.atoms :refer [document-title]]
+            [rems.atoms :refer [document-title close-symbol]]
             [rems.collapsible :as collapsible]
+            [rems.config :as config]
             [rems.guide-functions]
-            [rems.search :as search]
-            [rems.text :refer [text]]))
+            [rems.spinner :as spinner]
+            [rems.text :refer [text]]
+            [rems.util :refer [fetch]]))
 
 (rf/reg-event-fx
  ::enter-page
  (fn [{:keys [db]} _]
-   {:db (dissoc db
-                ::todo-applications
-                ::handled-applications)
-    :dispatch-n [[::todo-applications]
+   {:db (-> db
+            (dissoc ::todo-applications
+                    ::handled-applications)
+            (assoc ::initializing-todo-applications? true
+                   ::initializing-handled-applications? true))
+    :dispatch-n [[::fetch-todo-applications]
                  [:rems.table/reset]]}))
 
-(search/reg-fetcher ::todo-applications "/api/applications/todo")
-(search/reg-fetcher ::handled-applications "/api/applications/handled")
+;;;; applications to do
+
+(rf/reg-event-fx
+ ::fetch-todo-applications
+ (fn [{:keys [db]} [_ query]]
+   ;; do only one fetch at a time - will retry after the pending fetch is finished
+   (when-not (::fetching-todo-applications? db)
+     (fetch "/api/applications/todo"
+            {:url-params (when query
+                           {:query query})
+             :handler #(rf/dispatch [::fetch-todo-applications-result % query])
+             ;; TODO: show error message
+             :error-handler #(rf/dispatch [::fetch-todo-applications-result nil query])}))
+   {:db (assoc db
+               ::todo-applications-query query
+               ::fetching-todo-applications? true)}))
+
+(rf/reg-event-db
+ ::fetch-todo-applications-result
+ (fn [db [_ result query]]
+   ;; fetch again if the query that just finished was not the latest
+   (when-not (= query (::todo-applications-query db))
+     (rf/dispatch [::fetch-todo-applications (::todo-applications-query db)]))
+   (-> db
+       (assoc ::todo-applications result)
+       (dissoc ::initializing-todo-applications?
+               ::fetching-todo-applications?))))
+
+(rf/reg-sub
+ ::todo-applications
+ (fn [db _]
+   (::todo-applications db)))
+
+(rf/reg-sub
+ ::initializing-todo-applications?
+ (fn [db _]
+   (::initializing-todo-applications? db)))
+
+(rf/reg-sub
+ ::searching-todo-applications?
+ (fn [db _]
+   (and (::fetching-todo-applications? db)
+        (not (::initializing-todo-applications? db)))))
+
+;;;; handled applications
+
+(rf/reg-event-fx
+ ::fetch-handled-applications
+ (fn [{:keys [db]} [_ query]]
+   ;; do only one fetch at a time - will retry after the pending fetch is finished
+   (when-not (::fetching-handled-applications? db)
+     (fetch "/api/applications/handled"
+            {:url-params (when query
+                           {:query query})
+             :handler #(rf/dispatch [::fetch-handled-applications-result % query])
+             ;; TODO: show error message
+             :error-handler #(rf/dispatch [::fetch-handled-applications-result nil query])}))
+   {:db (assoc db
+               ::handled-applications-query query
+               ::fetching-handled-applications? true)}))
+
+(rf/reg-event-db
+ ::fetch-handled-applications-result
+ (fn [db [_ result query]]
+   ;; fetch again if the query that just finished was not the latest
+   (when-not (= query (::handled-applications-query db))
+     (rf/dispatch [::fetch-handled-applications (::handled-applications-query db)]))
+   (-> db
+       (assoc ::handled-applications result)
+       (dissoc ::initializing-handled-applications?
+               ::fetching-handled-applications?))))
+
+(rf/reg-sub
+ ::handled-applications
+ (fn [db _]
+   (::handled-applications db)))
+
+(rf/reg-sub
+ ::initializing-handled-applications?
+ (fn [db _]
+   (::initializing-handled-applications? db)))
+
+(rf/reg-sub
+ ::searching-handled-applications?
+ (fn [db _]
+   (and (::fetching-handled-applications? db)
+        (not (::initializing-handled-applications? db)))))
 
 ;;;; UI
 
@@ -49,6 +139,81 @@
    [show-publications-button]
    [show-throughput-times-button]])
 
+(defn application-list-defaults []
+  (let [config @(rf/subscribe [:rems.config/config])
+        id-column (get config :application-id-column :id)]
+    {:visible-columns #{id-column :description :resource :applicant :state :submitted :last-activity :view}
+     :default-sort-column :last-activity
+     :default-sort-order :desc}))
+
+(defn- todo-applications []
+  (let [applications ::todo-applications]
+    (cond
+      @(rf/subscribe [::initializing-todo-applications?])
+      [spinner/big]
+
+      (empty? @(rf/subscribe [applications]))
+      [:div.actions.alert.alert-success (text :t.actions/empty)]
+
+      :else
+      [application-list/component
+       (-> (application-list-defaults)
+           (assoc :id applications
+                  :applications applications))])))
+
+(defn- handled-applications []
+  (let [applications ::handled-applications]
+    (cond
+      @(rf/subscribe [::initializing-handled-applications?])
+      [spinner/big]
+
+      (empty? @(rf/subscribe [applications]))
+      [:div.actions.alert.alert-success (text :t.actions/no-handled-yet)]
+
+      :else
+      [application-list/component
+       (-> (application-list-defaults)
+           (update :visible-columns disj :submitted)
+           (assoc :id applications
+                  :applications applications))])))
+
+(defn- search-field [{:keys [id on-search searching?]}]
+  (let [input-value (r/atom "")
+        input-element (atom nil)]
+    ;; TODO: localization & aria-labels
+    (fn [{:keys [id on-search searching?]}]
+      [:div.search-field.mb-3
+       [:label.mr-1 {:for id}
+        "Search"]
+
+       [:div.input-group.mr-2
+        [:input.form-control
+         {:id id
+          :type :text
+          :value @input-value
+          :ref (fn [element]
+                 (reset! input-element element))
+          :on-change (fn [event]
+                       (let [value (-> event .-target .-value)]
+                         (reset! input-value value)
+                         (on-search value)))}]
+
+        (when-not (= "" @input-value)
+          [:div.input-group-append
+           [:button.btn.btn-outline-secondary
+            {:id (str id "-clear")
+             :type :button
+             ;; override the custom font-size from .btn which breaks .input-group
+             :style {:font-size "inherit"}
+             :on-click (fn []
+                         (reset! input-value "")
+                         (on-search "")
+                         (.focus @input-element))}
+            [close-symbol]]])]
+
+       (when searching?
+         [spinner/small])])))
+
 (defn actions-page []
   [:div
    [document-title (text :t.navigation/actions)]
@@ -58,24 +223,18 @@
       :open? true
       :title (text :t.actions/todo-applications)
       :collapse [:<>
-                 [search/search-field {:id "todo-search"
-                                       :on-search #(rf/dispatch [::todo-applications %])
-                                       :searching? @(rf/subscribe [::todo-applications :searching?])}]
-                 [application-list/component {:applications ::todo-applications
-                                              :empty-message :t.actions/empty
-                                              :hidden-columns #{:created}
-                                              :default-sort-column :last-activity
-                                              :default-sort-order :desc}]]}]
+                 (when (config/dev-environment?) ; TODO: remove feature flag
+                   [search-field {:id "todo-search"
+                                  :on-search #(rf/dispatch [::fetch-todo-applications %])
+                                  :searching? @(rf/subscribe [::searching-todo-applications?])}])
+                 [todo-applications]]}]
     [collapsible/component
      {:id "handled-applications"
-      :on-open #(rf/dispatch [::handled-applications])
+      :on-open #(rf/dispatch [::fetch-handled-applications])
       :title (text :t.actions/handled-applications)
       :collapse [:<>
-                 [search/search-field {:id "handled-search"
-                                       :on-search #(rf/dispatch [::handled-applications %])
-                                       :searching? @(rf/subscribe [::handled-applications :searching?])}]
-                 [application-list/component {:applications ::handled-applications
-                                              :empty-message :t.actions/no-handled-yet
-                                              :hidden-columns #{:created :submitted}
-                                              :default-sort-column :last-activity
-                                              :default-sort-order :desc}]]}]]])
+                 (when (config/dev-environment?) ; TODO: remove feature flag
+                   [search-field {:id "handled-search"
+                                  :on-search #(rf/dispatch [::fetch-handled-applications %])
+                                  :searching? @(rf/subscribe [::searching-handled-applications?])}])
+                 [handled-applications]]}]]])
diff --git a/src/cljs/rems/application_list.cljs b/src/cljs/rems/application_list.cljs
index 0fae35115..205950788 100644
--- a/src/cljs/rems/application_list.cljs
+++ b/src/cljs/rems/application_list.cljs
@@ -1,11 +1,9 @@
 (ns rems.application-list
-  (:require [clojure.set :as set]
-            [clojure.string :as str]
+  (:require [clojure.string :as str]
             [re-frame.core :as rf]
             [rems.application-util :as application-util]
             [rems.atoms :as atoms]
             [rems.guide-functions]
-            [rems.spinner :as spinner]
             [rems.table :as table]
             [rems.text :refer [localize-state localize-time localized text]])
   (:require-macros [rems.guide-macros :refer [component-info example]]))
@@ -69,8 +67,8 @@
            :view {:td [:td.view [view-button app]]}})
         apps)))
 
-(defn list [{:keys [id applications visible-columns default-sort-column default-sort-order]
-             :or {visible-columns (constantly true)}}]
+(defn component [{:keys [id applications visible-columns default-sort-column default-sort-order filterable?]
+                  :or {visible-columns (constantly true) filterable? true}}]
   (let [all-columns [{:key :id
                       :title (text :t.applications/id)}
                      {:key :external-id
@@ -97,31 +95,10 @@
                            :rows [::table-rows applications]
                            :default-sort-column default-sort-column
                            :default-sort-order default-sort-order}]
-    [table/table application-table]))
-
-(defn- application-list-defaults []
-  (let [config @(rf/subscribe [:rems.config/config])
-        id-column (get config :application-id-column :id)]
-    {:visible-columns #{id-column :description :resource :applicant :state :created :submitted :last-activity :view}
-     :default-sort-column :created
-     :default-sort-order :desc}))
-
-(defn component
-  "An application list which shows a spinner on initial page load (meant to be
-  used with rems.search/reg-fetcher) and a message if there are no applications."
-  [{:keys [applications empty-message hidden-columns] :as opts}]
-  (cond
-    (not @(rf/subscribe [applications :initialized?]))
-    [spinner/big]
-
-    (empty? @(rf/subscribe [applications]))
-    [:div.applications.alert.alert-success (text (or empty-message :t.applications/empty))]
-
-    :else
-    [list (-> (application-list-defaults)
-              (update :visible-columns set/difference (set hidden-columns))
-              (assoc :id applications)
-              (merge opts))]))
+    [:div
+     (when filterable?
+       [table/search application-table])
+     [table/table application-table]]))
 
 (defn guide []
   (rf/reg-sub
@@ -175,17 +152,17 @@
        :application/last-activity "2017-01-01T01:01:01:001Z"}]))
 
   [:div
-   (component-info list)
+   (component-info component)
    (example "empty list"
-            [list {:id ::example1
-                   :applications ::no-applications
-                   :visible-columns #{:id :description :resource :applicant :state :created :last-activity :view}}])
+            [component {:id ::example1
+                        :applications ::no-applications
+                        :visible-columns #{:id :description :resource :applicant :state :created :last-activity :view}}])
    (example "applications, default order"
-            [list {:id ::example2
-                   :applications ::example-applications
-                   :visible-columns #{:id :description :resource :applicant :state :created :last-activity :view}}])
+            [component {:id ::example2
+                        :applications ::example-applications
+                        :visible-columns #{:id :description :resource :applicant :state :created :last-activity :view}}])
    (example "applications, descending date, all columns"
-            [list {:id ::example3
-                   :applications ::example-applications
-                   :default-sort-column :created
-                   :default-sort-order :desc}])])
+            [component {:id ::example3
+                        :applications ::example-applications
+                        :default-sort-column :created
+                        :default-sort-order :desc}])])
diff --git a/src/cljs/rems/applications.cljs b/src/cljs/rems/applications.cljs
index 0cfb8e905..1460c4583 100644
--- a/src/cljs/rems/applications.cljs
+++ b/src/cljs/rems/applications.cljs
@@ -3,49 +3,102 @@
             [rems.application-list :as application-list]
             [rems.atoms :refer [document-title]]
             [rems.roles :as roles]
-            [rems.search :as search]
             [rems.spinner :as spinner]
-            [rems.text :refer [text]]))
+            [rems.text :refer [localize-state localize-time text]]
+            [rems.util :refer [fetch]]))
 
 (rf/reg-event-fx
  ::enter-page
  (fn [{:keys [db]} _]
-   {:db (dissoc db
-                ::my-applications
-                ::all-applications)
-    :dispatch-n [[::my-applications]
+   {:db (dissoc db ::my-applications ::all-applications)
+    :dispatch-n [[::fetch-my-applications]
                  (when (roles/show-all-applications? (:roles (:identity db)))
-                   [::all-applications])
+                   [::fetch-all-applications])
                  [:rems.table/reset]]}))
 
-(search/reg-fetcher ::my-applications "/api/my-applications")
-(search/reg-fetcher ::all-applications "/api/applications")
+;;;; my applications
+
+(rf/reg-event-fx
+ ::fetch-my-applications
+ (fn [{:keys [db]} _]
+   (fetch "/api/my-applications"
+          {:handler #(rf/dispatch [::fetch-my-applications-result %])})
+   {:db (assoc db ::loading-my-applications? true)}))
+
+(rf/reg-event-db
+ ::fetch-my-applications-result
+ (fn [db [_ applications]]
+   (-> db
+       (assoc ::my-applications applications)
+       (dissoc ::loading-my-applications?))))
+
+(rf/reg-sub
+ ::my-applications
+ (fn [db _]
+   (::my-applications db)))
+
+(rf/reg-sub
+ ::loading-my-applications?
+ (fn [db _]
+   (::loading-my-applications? db)))
+
+;;;; all applications
+
+(rf/reg-event-fx
+ ::fetch-all-applications
+ (fn [{:keys [db]} _]
+   (fetch "/api/applications"
+          {:handler #(rf/dispatch [::fetch-all-applications-result %])})
+   {:db (assoc db ::loading-all-applications? true)}))
+
+(rf/reg-event-db
+ ::fetch-all-applications-result
+ (fn [db [_ applications]]
+   (-> db
+       (assoc ::all-applications applications)
+       (dissoc ::loading-all-applications?))))
+
+(rf/reg-sub
+ ::all-applications
+ (fn [db _]
+   (::all-applications db)))
+
+(rf/reg-sub
+ ::loading-all-applications?
+ (fn [db _]
+   (::loading-all-applications? db)))
 
 ;;;; UI
 
+(defn- application-list [opts]
+  (let [apps @(rf/subscribe [(:applications opts)])]
+    (cond (::loading? opts)
+          [spinner/big]
+
+          (empty? apps)
+          [:div.applications.alert.alert-success (text :t.applications/empty)]
+
+          :else
+          (let [config @(rf/subscribe [:rems.config/config])
+                id-column (get config :application-id-column :id)]
+            [application-list/component
+             (merge {:visible-columns #{id-column :description :resource :state :created :submitted :last-activity :view}
+                     :default-sort-column :created
+                     :default-sort-order :desc}
+                    opts)]))))
+
 (defn applications-page []
   (let [identity @(rf/subscribe [:identity])]
-    [:<>
+    [:div
      [document-title (text :t.applications/applications)]
-     (if (not @(rf/subscribe [::my-applications :initialized?]))
-       [spinner/big]
-       [:<>
-        (when (roles/show-all-applications? (:roles identity))
-          [:h2 (text :t.applications/my-applications)])
-        [search/search-field {:id "my-applications-search"
-                              :on-search #(rf/dispatch [::my-applications %])
-                              :searching? @(rf/subscribe [::my-applications :searching?])}]
-        [application-list/component {:applications ::my-applications
-                                     :hidden-columns #{:applicant}
-                                     :default-sort-column :created
-                                     :default-sort-order :desc}]
-        (when (roles/show-all-applications? (:roles identity))
-          [:<>
-           [:h2 (text :t.applications/all-applications)]
-           [search/search-field {:id "all-applications-search"
-                                 :on-search #(rf/dispatch [::all-applications %])
-                                 :searching? @(rf/subscribe [::all-applications :searching?])}]
-           [application-list/component {:applications ::all-applications
-                                        :hidden-columns #{:created :submitted}
-                                        :default-sort-column :last-activity
-                                        :default-sort-order :desc}]])])]))
+     (when (roles/show-all-applications? (:roles identity))
+       [:h2 (text :t.applications/my-applications)])
+     [application-list {:id ::my-applications
+                        :applications ::my-applications
+                        ::loading? @(rf/subscribe [::loading-my-applications?])}]
+     (when (roles/show-all-applications? (:roles identity))
+       [:div
+        [:h2 (text :t.applications/all-applications)]
+        [application-list {:id ::all-applications
+                           :applications ::all-applications
+                           ::loading? @(rf/subscribe [::loading-all-applications?])}]])]))
diff --git a/src/cljs/rems/catalogue.cljs b/src/cljs/rems/catalogue.cljs
index 1f8e8b22d..365aec61a 100644
--- a/src/cljs/rems/catalogue.cljs
+++ b/src/cljs/rems/catalogue.cljs
@@ -100,12 +100,13 @@
       [:div
        [:h2 (text :t.catalogue/continue-existing-application)]
        (text :t.catalogue/continue-existing-application-intro)
-       [application-list/list
+       [application-list/component
         {:id applications
          :applications applications
          :visible-columns #{:resource :last-activity :view}
          :default-sort-column :last-activity
-         :default-sort-order :desc}]])))
+         :default-sort-order :desc
+         :filterable? false}]])))
 
 (defn- catalogue-table []
   (let [catalogue {:id ::catalogue
diff --git a/src/cljs/rems/search.cljs b/src/cljs/rems/search.cljs
deleted file mode 100644
index 48db6a9c0..000000000
--- a/src/cljs/rems/search.cljs
+++ /dev/null
@@ -1,99 +0,0 @@
-(ns rems.search
-  (:require [re-frame.core :as rf]
-            [reagent.core :as r]
-            [rems.atoms :refer [close-symbol]]
-            [rems.spinner :as spinner]
-            [rems.text :refer [text]]
-            [rems.util :refer [fetch]]))
-
-(defn reg-fetcher
-  "Registers a set of event handlers and subscriptions for fetching data, with
-  support for search. Throttles automatically so that only one fetch is running
-  at a time.
-  
-  Given `(reg-fetcher ::foo \"/url\")`, registers the following:
-
-  - Handler `[::foo]` fetches data from `/url`, i.e. get all items
-  - Handler `[::foo \"bar\"]` fetches data from `/url?query=bar`, i.e. do a search
-  - Subscription `[::foo]` returns the data that was last fetched
-  - Subscription `[::foo :initialized?]` returns `true` if the data has been
-    fetched at least once
-  - Subscription `[::foo :fetching?]` returns `true` if a fetch is in progress
-  - Subscription `[::foo :searching?]` returns `true` if a search is in progress,
-    i.e. a fetch after the initial fetch
-
-  All state is stored in `db` under `::foo`, which can be removed to reset state."
-  [id url]
-  (let [result-id (keyword (namespace id)
-                           (str (name id) "-result"))]
-    (rf/reg-event-fx
-     id
-     (fn [{:keys [db]} [_ query]]
-       ;; do only one fetch at a time - will retry after the pending fetch is finished
-       (when-not (get-in db [id :fetching?])
-         (fetch url
-                {:url-params (when query
-                               {:query query})
-                 :handler #(rf/dispatch [result-id % query])
-                 ;; TODO: show error message
-                 :error-handler #(rf/dispatch [result-id nil query])}))
-       {:db (-> db
-                (assoc-in [id :query] query)
-                (assoc-in [id :fetching?] true))}))
-
-    (rf/reg-event-db
-     result-id
-     (fn [db [_ result query]]
-       ;; fetch again if the query that just finished was not the latest
-       (let [latest-query (get-in db [id :query])]
-         (when-not (= query latest-query)
-           (rf/dispatch [id latest-query])))
-       (-> db
-           (assoc-in [id :data] result)
-           (assoc-in [id :initialized?] true)
-           (assoc-in [id :fetching?] false))))
-
-    (rf/reg-sub
-     id
-     (fn [db [_ k]]
-       (case k
-         :searching? (and (get-in db [id :fetching?])
-                          (get-in db [id :initialized?]))
-         (get-in db [id (or k :data)]))))))
-
-(defn search-field [{:keys [id on-search searching?]}]
-  (let [input-value (r/atom "")
-        input-element (atom nil)]
-    (fn [{:keys [id on-search searching?]}]
-      [:div.search-field.mb-3
-       [:label.mr-1 {:for id}
-        (text :t.search/search)]
-
-       [:div.input-group.mr-2
-        [:input.form-control
-         {:id id
-          :type :text
-          :value @input-value
-          :ref (fn [element]
-                 (reset! input-element element))
-          :on-change (fn [event]
-                       (let [value (-> event .-target .-value)]
-                         (reset! input-value value)
-                         (on-search value)))}]
-
-        (when-not (= "" @input-value)
-          [:div.input-group-append
-           [:button.btn.btn-outline-secondary
-            {:id (str id "-clear")
-             :type :button
-             :aria-label (text :t.search/clear-search)
-             ;; override the custom font-size from .btn which breaks .input-group
-             :style {:font-size "inherit"}
-             :on-click (fn []
-                         (reset! input-value "")
-                         (on-search "")
-                         (.focus @input-element))}
-            [close-symbol]]])]
-
-       (when searching?
-         [spinner/small])])))
diff --git a/src/cljs/rems/table.cljs b/src/cljs/rems/table.cljs
index bba03a4f1..20b680887 100644
--- a/src/cljs/rems/table.cljs
+++ b/src/cljs/rems/table.cljs
@@ -3,7 +3,8 @@
             [clojure.string :as str]
             [re-frame.core :as rf]
             [rems.atoms :refer [close-symbol search-symbol sort-symbol]]
-            [rems.search :as search])
+            [rems.focus :as focus]
+            [rems.text :refer [text]])
   (:require-macros [rems.guide-macros :refer [component-info example]]))
 
 (rf/reg-event-db
@@ -172,12 +173,38 @@
                  (assoc row ::display-row? (display-row? row columns filters))))))))
 
 (defn search [table]
-  (let [filtering @(rf/subscribe [::filtering table])
-        on-search (fn [value]
-                    (rf/dispatch [::set-filtering table (assoc filtering :filters value)]))]
-    [search/search-field {:id (str (name (:id table)) "-search")
-                          :on-search on-search
-                          :searching? false}]))
+  (let [search-params (atom nil)
+        open-search (atom nil)]
+    (fn [table]
+      (let [filtering @(rf/subscribe [::filtering table])
+            on-search (fn [event]
+                        (rf/dispatch [::set-filtering table (-> filtering
+                                                                (assoc :filters (-> event .-target .-value)))]))
+            on-toggle (fn [_event]
+                        (focus/set-focus (if (:show-filters filtering)
+                                           open-search
+                                           search-params))
+                        (rf/dispatch [::set-filtering table (-> filtering
+                                                                (update :show-filters not)
+                                                                (assoc :filters ""))]))]
+        (if (:show-filters filtering)
+          [:div.rems-table-search-toggle.d-flex.flex-row
+           [:div.flex-grow-1.d-flex
+            [:input.flex-grow-1 {:type :text
+                                 :default-value (:filters filtering)
+                                 :aria-label (text :t.search/search-parameters)
+                                 :on-change on-search
+                                 :ref #(focus/ref-changed search-params %)}]]
+           [:button.btn.btn-secondary {:type :button
+                                       :aria-label (text :t.search/close-search)
+                                       :on-click on-toggle}
+            [close-symbol]]]
+          [:div.rems-table-search-toggle.d-flex.flex-row-reverse
+           [:button.btn.btn-primary {:type :button
+                                     :aria-label (text :t.search/open-search)
+                                     :on-click on-toggle
+                                     :ref #(focus/ref-changed open-search %)}
+            [search-symbol]]])))))
 
 (defn- table-header [table]
   (let [sorting @(rf/subscribe [::sorting table])]


Test patch:
diff --git a/src/clj/rems/db/test_data.clj b/src/clj/rems/db/test_data.clj
index 60d40847a..95e0d9186 100644
--- a/src/clj/rems/db/test_data.clj
+++ b/src/clj/rems/db/test_data.clj
@@ -1,11 +1,11 @@
 (ns rems.db.test-data
   "Populating the database with nice test data."
   (:require [clj-time.core :as time]
-            [rems.api.services.licenses :as licenses]
             [rems.db.applications :as applications]
             [rems.db.catalogue :as catalogue]
             [rems.db.core :as db]
             [rems.db.form :as form]
+            [rems.db.licenses :as licenses]
             [rems.db.resource :as resource]
             [rems.db.roles :as roles]
             [rems.db.users :as users]
@@ -178,7 +178,7 @@
     (doseq [role roles]
       (roles/add-role! user role))))
 
-(defn create-users-and-roles! []
+(defn- create-users-and-roles! []
   ;; users provided by the fake login
   (let [users (comp +fake-user-data+ +fake-users+)]
     (create-user! (users :applicant1))
diff --git a/test/clj/rems/api/testing.clj b/test/clj/rems/api/testing.clj
index c59e8b381..22fc99b81 100644
--- a/test/clj/rems/api/testing.clj
+++ b/test/clj/rems/api/testing.clj
@@ -3,7 +3,7 @@
   (:require [clojure.test :refer :all]
             [cheshire.core :refer [parse-stream]]
             [mount.core :as mount]
-            [rems.db.testing :refer [test-data-fixture test-db-fixture caches-fixture search-index-fixture]]
+            [rems.db.testing :refer [test-data-fixture test-db-fixture caches-fixture]]
             [rems.handler :refer :all]
             [ring.mock.request :refer :all]))
 
@@ -17,7 +17,6 @@
 
 (def api-fixture
   (join-fixtures [test-db-fixture
-                  search-index-fixture
                   caches-fixture
                   test-data-fixture
                   handler-fixture]))
diff --git a/test/clj/rems/application/test_search.clj b/test/clj/rems/application/test_search.clj
index e475f93c2..b4dfd8f19 100644
--- a/test/clj/rems/application/test_search.clj
+++ b/test/clj/rems/application/test_search.clj
@@ -1,126 +1,30 @@
 (ns ^:integration rems.application.test-search
   (:require [clojure.test :refer :all]
             [rems.application.search :as search]
-            [rems.db.applications :as applications]
             [rems.db.test-data :as test-data]
-            [rems.db.testing :refer [test-db-fixture search-index-fixture]]))
+            [rems.db.testing :refer [test-db-fixture rollback-db-fixture]]))
 
-(use-fixtures :once (join-fixtures [test-db-fixture
-                                    search-index-fixture]))
+(use-fixtures :once test-db-fixture)
+(use-fixtures :each rollback-db-fixture)
 
 (deftest test-application-search
-  ;; generate users with full names and emails
-  (test-data/create-users-and-roles!)
   ;; unrelated application - it's an error if any of the tests finds this
   (test-data/create-application! {:actor "developer"})
+  (search/refresh!)
 
   (testing "find by applicant"
     (let [app-id (test-data/create-application! {:actor "alice"})]
-      (is (= #{app-id} (search/find-applications "alice")) "user ID, any field")
-      (is (= #{app-id} (search/find-applications "applicant:alice")) "user ID")
-      (is (= #{app-id} (search/find-applications "applicant:\"Alice Applicant\"")) "name")
-      (is (= #{app-id} (search/find-applications "applicant:\"alice@example.com\"")) "email")))
+      (is (= #{app-id} (search/find-applications "alice")))))
 
-  (testing "find by member"
-    (let [app-id (test-data/create-application! {:actor "alice"})]
-      (test-data/command! {:type :application.command/submit
-                           :application-id app-id
-                           :actor "alice"})
-      (test-data/command! {:type :application.command/add-member
-                           :application-id app-id
-                           :actor "developer"
-                           :member {:userid "bob"}})
-      (is (= #{app-id} (search/find-applications "bob")) "user ID, any field")
-      (is (= #{app-id} (search/find-applications "member:bob")) "user ID")
-      (is (= #{app-id} (search/find-applications "member:\"Bob Approver\"")) "name")
-      (is (= #{app-id} (search/find-applications "member:\"bob@example.com\"")) "email")))
-
-  (testing "find by ID"
-    (let [app-id (test-data/create-application! {:actor "alice"})
-          app (applications/get-unrestricted-application app-id)]
-      (is (= #{app-id} (search/find-applications (str app-id))) "app ID, any field")
-      (is (= #{app-id} (search/find-applications (str "id:" app-id))) "app ID")
-      (is (= #{app-id} (search/find-applications (str "id:\"" (:application/external-id app) "\""))) "external ID")))
-
-  (testing "find by title"
-    (let [form-id (test-data/create-form! {:form/fields [{:field/type :description
-                                                          :field/title {:en "Title"}
-                                                          :field/optional false}]})
-          cat-id (test-data/create-catalogue-item! {:form-id form-id})
-          app-id (test-data/create-application! {:catalogue-item-ids [cat-id]
-                                                 :actor "alice"})]
-      (test-data/command! {:type :application.command/save-draft
-                           :application-id app-id
-                           :actor "alice"
-                           :field-values [{:field 1
-                                           :value "Supercalifragilisticexpialidocious"}]})
-      (is (= #{app-id} (search/find-applications "Supercalifragilisticexpialidocious")) "any field")
-      (is (= #{app-id} (search/find-applications "title:Supercalifragilisticexpialidocious")) "title field")))
-
-  (testing "find by resource"
-    (let [cat-id (test-data/create-catalogue-item! {:title {:en "Spam"
-                                                            :fi "Nötkötti"}})
-          app-id (test-data/create-application! {:catalogue-item-ids [cat-id]
-                                                 :actor "alice"})]
-      (is (= #{app-id} (search/find-applications "Spam")) "en title, any field")
-      (is (= #{app-id} (search/find-applications "resource:Spam")) "en title")
-      (is (= #{app-id} (search/find-applications "resource:Nötkötti")) "fi title")))
-
-  (testing "find by state"
-    (let [app-id (test-data/create-application! {:actor "alice"})]
-      (test-data/command! {:type :application.command/submit
-                           :application-id app-id
-                           :actor "alice"})
-      (test-data/command! {:type :application.command/approve
-                           :application-id app-id
-                           :actor "developer"
-                           :comment ""})
-      (is (= #{app-id} (search/find-applications "Approved")) "en status, any field")
-      (is (= #{app-id} (search/find-applications "state:Approved")) "en status")
-      (is (= #{app-id} (search/find-applications "state:Hyväksytty")) "fi status")))
+  (testing "find by member")
 
-  (testing "find by form content"
-    (let [form-id (test-data/create-form! {:form/fields [{:field/type :text
-                                                          :field/title {:en "Text field"}
-                                                          :field/optional false}]})
-          cat-id (test-data/create-catalogue-item! {:form-id form-id})
-          app-id (test-data/create-application! {:catalogue-item-ids [cat-id]
-                                                 :actor "alice"})]
-      (test-data/command! {:type :application.command/save-draft
-                           :application-id app-id
-                           :actor "alice"
-                           :field-values [{:field 1
-                                           :value "Tis but a scratch."}]})
-      (is (= #{app-id} (search/find-applications "scratch")) "any field")
-      (is (= #{app-id} (search/find-applications "form:scratch")) "form field")))
+  (testing "find by title")
 
-  (testing "updating applications"
-    (let [form-id (test-data/create-form! {:form/fields [{:field/type :text
-                                                          :field/title {:en "Text field"}
-                                                          :field/optional false}]})
-          cat-id (test-data/create-catalogue-item! {:form-id form-id})
-          app-id (test-data/create-application! {:catalogue-item-ids [cat-id]
-                                                 :actor "alice"})]
-      (test-data/command! {:type :application.command/save-draft
-                           :application-id app-id
-                           :actor "alice"
-                           :field-values [{:field 1
-                                           :value "version1"}]})
-      (is (= #{app-id} (search/find-applications "version1"))
-          "original version is indexed")
+  (testing "find by resource")
 
-      (test-data/command! {:type :application.command/save-draft
-                           :application-id app-id
-                           :actor "alice"
-                           :field-values [{:field 1
-                                           :value "version2"}]})
-      (is (= #{} (search/find-applications "version1"))
-          "should not find old versions")
-      (is (= #{app-id} (search/find-applications "version2"))
-          "should find the new version")))
+  (testing "find by state")
 
-  (testing "multiple results"
-    (is (< 1 (count (search/find-applications "alice")))))
+  (testing "find by form content")
 
   (testing "invalid query"
     (is (= nil (search/find-applications "+")))))
diff --git a/test/clj/rems/db/testing.clj b/test/clj/rems/db/testing.clj
index 79413e851..c52d0cd10 100644
--- a/test/clj/rems/db/testing.clj
+++ b/test/clj/rems/db/testing.clj
@@ -5,30 +5,23 @@
             [conman.core :as conman]
             [luminus-migrations.core :as migrations]
             [mount.core :as mount]
-            [rems.application.search]
             [rems.config :refer [env]]
             [rems.db.applications]
             [rems.db.core :as db]
-            [rems.db.test-data :as test-data]
-            [rems.locales])
+            [rems.db.test-data :as test-data])
   (:import [org.joda.time Duration ReadableInstant]))
 
 (defn test-db-fixture [f]
   (mount/stop) ;; during interactive development, app might be running when tests start. we need to tear it down
   (mount/start-with-args {:test true}
                          #'rems.config/env
-                         #'rems.db.core/*db*)
+                         #'rems.db.core/*db*
+                         #'rems.application.search/search-index)
   (db/assert-test-database!)
   (migrations/migrate ["reset"] {:database-url (:test-database-url env)})
   (f)
   (mount/stop))
 
-(defn search-index-fixture [f]
-  ;; no specific teardown. relies on the teardown of test-db-fixture.
-  (mount/start #'rems.application.search/search-index
-               #'rems.locales/translations)
-  (f))
-
 (defn caches-fixture [f]
   ;; no specific teardown. relies on the teardown of test-db-fixture.
   (mount/start #'rems.db.applications/application-cache
diff --git a/test/clj/rems/poller/test_email.clj b/test/clj/rems/poller/test_email.clj
index dcb22a07a..d56fd9b4e 100644
--- a/test/clj/rems/poller/test_email.clj
+++ b/test/clj/rems/poller/test_email.clj
@@ -16,29 +16,6 @@
     (f)
     (mount/stop)))
 
-(deftest test-send-email!
-  ;; Just for a bit of coverage in code that doesn't get run in other tests or the dev profile
-  (let [message-atom (atom nil)]
-    (with-redefs [rems.config/env (assoc rems.config/env
-                                         :smtp-host "localhost"
-                                         :smtp-port 25
-                                         :mail-from "rems@rems.rems")
-                  postal.core/send-message (fn [_host message] (reset! message-atom message))
-                  rems.db.users/get-user-attributes (constantly {:mail "user@example.com"})]
-      (send-email! {:to "foo@example.com" :subject "ding" :body "boing"})
-      (is (= {:to "foo@example.com"
-              :subject "ding"
-              :body "boing"
-              :from "rems@rems.rems"}
-             @message-atom))
-      (send-email! {:to-user "user" :subject "ding" :body "boing"})
-      (is (= {:to "user@example.com"
-              :to-user "user"
-              :subject "ding"
-              :body "boing"
-              :from "rems@rems.rems"}
-             @message-atom)))))
-
 (defn sort-emails [emails]
   (sort-by #(or (:to %) (:to-user %)) emails))
 
@@ -127,62 +104,62 @@
       (is (= [[]
               [{:to-user "assistant",
                 :subject "Application submitted (applicant: 2001/3)",
-                :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}
                {:to-user "handler",
                 :subject "Application submitted (applicant: 2001/3)",
-                :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}]
               [{:to "somebody@example.com",
                 :subject "Invitation to participate in an application",
-                :body "Hello,\n\nThis email address (somebody@example.com) has been invited to participate in an application.\n\nParticipate: http://example.com/accept-invitation?token=abc\n\nPlease don't reply to this automatically generated message."}]
+                :body "Hello,\n\nThis email address (somebody@example.com) has been invited to participate in an application.\n\nParticipate: http://example.com/accept-invitation?token=abc"}]
               [{:to-user "commenter1",
                 :subject "Review requested (handler: 2001/3)",
-                :body "Dear commenter1,\n\nhandler has requested a review on application 2001/3.\n\nComment: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear commenter1,\n\nhandler has requested a review on application 2001/3.\n\nComment: http://example.com/#/application/7"}
                {:to-user "commenter2",
                 :subject "Review requested (handler: 2001/3)",
-                :body "Dear commenter2,\n\nhandler has requested a review on application 2001/3.\n\nComment: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                :body "Dear commenter2,\n\nhandler has requested a review on application 2001/3.\n\nComment: http://example.com/#/application/7"}]
               []
               [{:to-user "assistant",
                 :subject "Review added (commenter2: 2001/3)",
-                :body "Dear assistant,\n\ncommenter2 has reviewed application 2001/3.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear assistant,\n\ncommenter2 has reviewed application 2001/3.\n\nView application: http://example.com/#/application/7"}
                {:to-user "handler",
                 :subject "Review added (commenter2: 2001/3)",
-                :body "Dear handler,\n\ncommenter2 has reviewed application 2001/3.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                :body "Dear handler,\n\ncommenter2 has reviewed application 2001/3.\n\nView application: http://example.com/#/application/7"}]
               [{:to-user "member",
                 :subject "Added as a member of an application (2001/3)",
-                :body "Dear member,\n\nYou've been added as a member of application 2001/3.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                :body "Dear member,\n\nYou've been added as a member of application 2001/3.\n\nView application: http://example.com/#/application/7"}]
               [{:to-user "decider",
                 :subject "Decision requested (assistant: 2001/3)",
-                :body "Dear decider,\n\nassistant has requested your decision on application 2001/3.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                :body "Dear decider,\n\nassistant has requested your decision on application 2001/3.\n\nView application: http://example.com/#/application/7"}]
               [{:to-user "assistant",
                 :subject "Decision made (decider: 2001/3)",
-                :body "Dear assistant,\n\ndecider has made a decision on application 2001/3.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear assistant,\n\ndecider has made a decision on application 2001/3.\n\nView application: http://example.com/#/application/7"}
                {:to-user "handler",
                 :subject "Decision made (decider: 2001/3)",
-                :body "Dear handler,\n\ndecider has made a decision on application 2001/3.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                :body "Dear handler,\n\ndecider has made a decision on application 2001/3.\n\nView application: http://example.com/#/application/7"}]
               [{:to-user "applicant",
                 :subject "Application approved (2001/3)",
-                :body "Dear applicant,\n\nYour application 2001/3 has been approved.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear applicant,\n\nYour application 2001/3 has been approved.\n\nView application: http://example.com/#/application/7"}
                {:to-user "assistant"
                 :subject "Application approved (2001/3)"
-                :body "Dear assistant,\n\nhandler has approved the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear assistant,\n\nhandler has approved the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7"}
                {:to-user "member",
                 :subject "Application approved (2001/3)",
-                :body "Dear member,\n\nYour application 2001/3 has been approved.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear member,\n\nYour application 2001/3 has been approved.\n\nView application: http://example.com/#/application/7"}
                {:to-user "somebody",
                 :subject "Application approved (2001/3)",
-                :body "Dear somebody,\n\nYour application 2001/3 has been approved.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                :body "Dear somebody,\n\nYour application 2001/3 has been approved.\n\nView application: http://example.com/#/application/7"}]
               [{:to-user "applicant",
                 :subject "Application closed (2001/3)",
-                :body "Dear applicant,\n\nYour application 2001/3 has been closed.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear applicant,\n\nYour application 2001/3 has been closed.\n\nView application: http://example.com/#/application/7"}
                {:to-user "handler"
                 :subject "Application closed (2001/3)"
-                :body "Dear handler,\n\nassistant has closed the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear handler,\n\nassistant has closed the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7"}
                {:to-user "member",
                 :subject "Application closed (2001/3)",
-                :body "Dear member,\n\nYour application 2001/3 has been closed.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                :body "Dear member,\n\nYour application 2001/3 has been closed.\n\nView application: http://example.com/#/application/7"}
                {:to-user "somebody",
                 :subject "Application closed (2001/3)",
-                :body "Dear somebody,\n\nYour application 2001/3 has been closed.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]]
+                :body "Dear somebody,\n\nYour application 2001/3 has been closed.\n\nView application: http://example.com/#/application/7"}]]
              (events-to-emails events))))
     (testing "application rejected"
       (let [events (conj base-events
@@ -192,15 +169,15 @@
         (is (= [[]
                 [{:to-user "assistant",
                   :subject "Application submitted (applicant: 2001/3)",
-                  :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                  :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}
                  {:to-user "handler",
                   :subject "Application submitted (applicant: 2001/3)",
-                  :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                  :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}]
                 [{:subject "Application rejected (2001/3)",
-                  :body "Dear applicant,\n\nYour application 2001/3 has been rejected.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message.",
+                  :body "Dear applicant,\n\nYour application 2001/3 has been rejected.\n\nView application: http://example.com/#/application/7",
                   :to-user "applicant"}
                  {:subject "Application rejected (2001/3)"
-                  :body "Dear assistant,\n\nhandler has rejected the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."
+                  :body "Dear assistant,\n\nhandler has rejected the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7"
                   :to-user "assistant"}]]
                (events-to-emails events)))))
     (testing "id field can be overrided"
@@ -208,10 +185,10 @@
         (is (= [[]
                 [{:to-user "assistant"
                   :subject "Application submitted (applicant: 7)"
-                  :body "Dear assistant,\n\napplicant has submitted an application (7): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                  :body "Dear assistant,\n\napplicant has submitted an application (7): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}
                  {:to-user "handler"
                   :subject "Application submitted (applicant: 7)"
-                  :body "Dear handler,\n\napplicant has submitted an application (7): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]]
+                  :body "Dear handler,\n\napplicant has submitted an application (7): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}]]
                (events-to-emails base-events)))))
     (testing "returning application to applicant and re-submitting"
       (let [events (conj base-events
@@ -225,20 +202,20 @@
         (is (= [[]
                 [{:to-user "assistant",
                   :subject "Application submitted (applicant: 2001/3)",
-                  :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                  :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}
                  {:to-user "handler",
                   :subject "Application submitted (applicant: 2001/3)",
-                  :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                  :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}]
                 [{:to-user "applicant",
                   :subject "Application returned (2001/3)",
-                  :body "Dear applicant,\n\nYour application 2001/3 has been returned.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                  :body "Dear applicant,\n\nYour application 2001/3 has been returned.\n\nView application: http://example.com/#/application/7"}
                  {:to-user "assistant",
                   :subject "Application returned (2001/3)",
-                  :body "Dear assistant,\n\nhandler has returned the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]
+                  :body "Dear assistant,\n\nhandler has returned the application 2001/3 from applicant.\n\nView application: http://example.com/#/application/7"}]
                 [{:to-user "assistant",
                   :subject "Application submitted (applicant: 2001/3)",
-                  :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}
+                  :body "Dear assistant,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}
                  {:to-user "handler",
                   :subject "Application submitted (applicant: 2001/3)",
-                  :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7\n\nPlease don't reply to this automatically generated message."}]]
+                  :body "Dear handler,\n\napplicant has submitted an application (2001/3): en title 11, en title 21.\n\nView application: http://example.com/#/application/7"}]]
                (events-to-emails events)))))))
diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj
index a69cc3f34..c8df288d7 100644
--- a/test/clj/rems/test_browser.clj
+++ b/test/clj/rems/test_browser.clj
@@ -95,6 +95,7 @@
 (defn go-to-applications []
   (click-navigation-menu "Applications")
   (wait-visible *driver* {:tag :h1, :fn/text "Applications"})
+  (wait-visible *driver* [{:css "i.fa-search"}])
   (wait-page-loaded)
   (screenshot *driver* (io/file reporting-dir "applications-page.png")))