diff --git a/.gitignore b/.gitignore
index f69db87ad5a5226535559b6965e771d975ded103..d2bd7089a35ed5496464734e2026397e3a802baa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,9 @@
 .*
 !/.git*
 
+# backup files
+*~
+
 # extracted libraries
 /libs/**
 !/libs/*.zip
@@ -26,3 +29,4 @@ xerr.log
 conf/ext
 test/ext
 src/ext
+*~
diff --git a/.gitlab/issue_templates/default.md b/.gitlab/issue_templates/default.md
new file mode 100644
index 0000000000000000000000000000000000000000..aa1a65aca363b87aff50280e1a86824009d2098b
--- /dev/null
+++ b/.gitlab/issue_templates/default.md
@@ -0,0 +1,28 @@
+## Summary
+
+*Please give a short summary of what the issue is.*
+
+## Expected Behavior
+
+*What did you expect how the software should behave?*
+
+## Actual Behavior
+
+*What did the software actually do?*
+
+## Steps to Reproduce the Problem
+
+*Please describe, step by step, how others can reproduce the problem.  Please try these steps for yourself on a clean system.*
+
+1.
+2.
+3.
+
+## Specifications
+
+- Version: *Which version of this software?*
+- Platform: *Which operating system, which other relevant software versions?*
+
+## Possible fixes
+
+*Do you have ideas how the issue can be resolved?*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aaace618e1ba063ee7ddd13148c96da71607f9f3..78608dd944b4c5b79762548e1a55f32e754d248a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,33 @@
 # Changelog
 All notable changes to this project will be documented in this file.
 
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.4.0] - 2021-10-28
+
+### Added (for new features, dependecies etc.)
+
+* Module `ext_qrcode` which generates a QR Code for an entity (pointing to the
+  the head or the exact version).
+* Optional functionality to bookmark all query results. Note that too many
+  bookmarks will result in the URI being too lang and bookmarks will have to be
+  cleared manually.
+
+### Changed (for changes in existing functionality)
+
+### Deprecated (for soon-to-be removed features)
+
+### Removed (for now removed features)
+
+* `getEntityId`, a former duplicate of `getEntityID` which must be used instead.
+
+### Fixed (for any bug fixes)
+
+### Security (in case of vulnerabilities)
+
+### Documentation (for notable additions or changes of the documentation)
+
 ## [v0.4.0-rc1] - 2021-06-16
 
 ### Added (for new features, dependecies etc.)
@@ -11,12 +35,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `ext_applicable` module for building fancy features which append
   functionality to entities (EXPERIMENTAL).
 - `ext_cosmetics` module which converts http(s) uris in property values into
-  clickable links.
+  clickable links (with tests)
 - Added a menu/toc for the tour
 - Added a previous and next buttons for pages in the tour
 - Added warnings to inform about minimum width when accessing tour and
   edit mode on small screens.
 - Added a tutorial for the edit mode to the documentation
+- Documentation on how to customize reference resolving
 
 ### Changed (for changes in existing functionality)
 
@@ -26,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   dropped entirely (e.g. "jumbotron"). Please have a look at
     * https://getbootstrap.com/docs/5.0/migration/
     * https://getbootstrap.com/docs/4.6/migration/
+- Moved the resolving of references to Person Records to separate
+  example which can be disabled
 
 ### Deprecated (for soon-to-be removed features)
 
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index d3dca09ef0ddd2c4d0d33f0134305a9c52691f09..c6043d09d049b91cfc0a03560970537feb8bbc06 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -1,2 +1,28 @@
-* CaosDB Server == 0.4
+* CaosDB Server 0.5.x
 * Make 4.2.0
+
+# Java Script Libraries (included in this repository)
+* bootstrap-5.0.1
+* bootstrap-autocomplete-2.3.5
+* bootstrap-icons-1.4.1
+* bootstrap-select-1.14.0-beta2
+* dropzone-5.5.0
+* javascript-state-machine-master
+* jquery-3.6.0.min.js
+* loglevel-1.6.4
+* qrcode-1.4.4
+* showdown-1.8.6
+* plotly.js-1.52.2
+* UTIF-8205c1f
+
+## For the map
+
+* leaflet-1.5.1
+* Leaflet.Coordinates-0.1.5
+* leaflet.latlng-graticule-20191007
+* L.Graticule.js https://github.com/turban/Leaflet.Graticule/blob/e9146fbea59ce1b0ada4ea2a012087f9a1a12473/L.Graticule.js
+* proj4js-2.5.0
+* Proj4Leaflet-1.0.1
+
+## For testing
+* qunit-2.9.2
diff --git a/Makefile b/Makefile
index 0aa47756d72dcfcaff88efb673b5fa5044f8e05a..4c64a38d01f0273059e29f6dac0448967b005b86 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ LIBS_DIR = $(abspath libs)
 TEST_CORE_DIR = $(abspath test/core/)
 TEST_EXT_DIR = $(abspath test/ext)
 TEST_SSS_DIR =$(abspath test/server_side_scripting)
-LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js
+LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js
 
 
 TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/))
@@ -299,6 +299,9 @@ $(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js
 $(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@
 
+$(LIBS_DIR)/js/qrcode.js: unzip $(LIBS_DIR)/js
+	ln -s $(LIBS_DIR)/qrcode-1.4.4/qrcode.min.js $@
+
 
 $(addprefix $(LIBS_DIR)/, js css):
 	mkdir $@ || true
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 58d20c521100bbd43e094c2da402abc38ea555bc..65a2b9bc42f9e0f22dc0cd6ca6baac094e94eee7 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -42,7 +42,6 @@
 # Modules enabled/disabled by default
 ##############################################################################
 BUILD_MODULE_EXT_PREVIEW=ENABLED
-BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED
 BUILD_MODULE_EXT_SSS_MARKDOWN=DISABLED
 BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=DISABLED
 BUILD_MODULE_EXT_AUTOCOMPLETE=ENABLED
@@ -50,11 +49,17 @@ BUILD_MODULE_EXT_BOTTOM_LINE=ENABLED
 BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW=DISABLED
 BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW=DISABLED
 BUILD_MODULE_EXT_BOOKMARKS=ENABLED
+BUILD_MODULE_EXT_ADD_QUERY_TO_BOOKMARKS=DISABLED
 BUILD_MODULE_EXT_ANNOTATION=ENABLED
+BUILD_MODULE_EXT_COSMETICS_LINKIFY=DISABLED
+BUILD_MODULE_EXT_QRCODE=ENABLED
 
 BUILD_MODULE_USER_MANAGEMENT=ENABLED
 BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB
 
+BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED
+BUILD_EXT_REFERENCES_CUSTOM_RESOLVER=person_reference
+
 ##############################################################################
 # Navbar properties
 ##############################################################################
@@ -148,4 +153,7 @@ MODULE_DEPENDENCIES=(
     ext_sss_markdown.js
     ext_trigger_crawler_form.js
     ext_bookmarks.js
+    ext_cosmetics.js
+    qrcode.js
+    ext_qrcode.js
 )
diff --git a/doc/QueryShortcuts/doc.pdf b/doc/QueryShortcuts/doc.pdf
deleted file mode 100644
index 0f7e46bb78b4f8c6cf7a30216cb9f507be747ad4..0000000000000000000000000000000000000000
Binary files a/doc/QueryShortcuts/doc.pdf and /dev/null differ
diff --git a/doc/QueryShortcuts/doc.tex b/doc/QueryShortcuts/doc.tex
deleted file mode 100644
index 4cd78391d1cda808dc9c332328d3a984ae5926d8..0000000000000000000000000000000000000000
--- a/doc/QueryShortcuts/doc.tex
+++ /dev/null
@@ -1,192 +0,0 @@
-\documentclass{article}
-% General document formatting
-\usepackage[margin=0.7in]{geometry}
-\usepackage[parfill]{parskip}
-\usepackage[utf8]{inputenc}
-\usepackage{graphicx}
-
-% Related to math
-\usepackage{amsmath,amssymb,amsfonts,amsthm}
-\title{Documentation Query Shortcuts}
-
-\begin{document}
-\maketitle
-\section{Introduction}\label{introduction}
-
-The WebUI supports the creation of query shortcuts which appear below
-the normal query input field. These shortcuts facilitate looking for
-data as query strings which are used frequently. They can be stored and
-reused.
-
-\begin{figure}[h]
-\centering
-\includegraphics[width=.8\textwidth]{shortcut_toolbox.png}
-\caption{The Shortcuts in the Query Panel; Note the Toolbox for in the top
-right}
-\end{figure}
-
-There are two ways to integrate query templates into the WebUI:
-
-\begin{itemize}
-\item
-  Global shortcuts are integrated by the webmaster only. They are
-  defined and stored in a\\
-  \texttt{./conf/ext/json/globale\_query\_shortcuts.json} in the root
-  directory of the webui.
-\item
-  User-defined templates can be defined by users and are only visible
-  for the user who created them. In this sense, user-defined shortcuts
-  are also private, whereas global shortcuts are always publicly
-  visible.
-\end{itemize}
-
-\section{User-defined Query Shortcuts}\label{user-defined-query-shortcuts}
-
-\subsection{Create a New Shortcut}\label{create-a-new-shortcut}
-
-New Query Shortcuts can be generated by any authenticated user with
-sufficient write permissions.
-
-In the web interface, click \texttt{Query}. In the \texttt{Shortcuts}
-section, click the wrench (on the right side).
-
-In the drop-down menu, click \texttt{Create}.
-
-It now opens a form with two input fields, \texttt{Description} and
-\texttt{Query}.
-\begin{figure}[h]
-\centering
-\includegraphics[width=.6\textwidth]{create_shortcut.png}
-\caption{The view to create a new shortcut}
-\end{figure}
-
-See \ref{basic-shortcut} and
-\ref{advanced-shortcut} for further
-explanation of the components of a Query Shortcut.
-
-Edit the fields and click \texttt{Submit} for the creation of the new
-shortcut or click \texttt{Cancel} to cancel the process.
-
-The new shortcut is shown in the shortcuts section.
-\begin{figure}[h]
-\centering
-\includegraphics[width=.6\textwidth]{create_success.png}
-\caption{The view when creation was successful}
-\end{figure}
-
-\subsection{Change an Existing Shortcut}\label{change-an-existing-shortcut}
-
-Existing Query Shortcuts which are visible in your shortcuts section can
-be edited directly in the shortcuts section.
-
-In the web interface, click \texttt{Query}. In the \texttt{Shortcuts}
-section, click the wrench (on the right side).
-
-In the drop-down menu, click \texttt{Edit}.
-
-\begin{figure}[h]
-\centering
-\includegraphics[width=.6\textwidth]{choose_edit.png}
-\caption{Choosing which shortcut to edit}
-\end{figure}
-Every editable shortcut (note: global shortcuts are not editable in the
-webinterface at all) will receive a new button \texttt{Edit}
-
-Click \texttt{Edit} of the shortcut that is to be changed.
-
-It now opens a form with two input fields, \texttt{Description} and
-\texttt{Query}, pre-filled.
-
-See \ref{basic-shortcut} and
-\ref{advanced-shortcut} for further
-explanation of the components of a Query Shortcut.
-
-Edit the fields and click \texttt{Submit} for the creation of the new
-shortcut or click \texttt{Cancel} to cancel the process.
-
-The updated shortcut is shown in the shortcuts section.
-
-See the
-
-\subsection{Delete an Existing Shortcut}\label{delete-an-existing-shortcut}
-
-Existing Query Shortcut which are visible in your shortcuts section can
-be edited directly in the shortcuts section.
-
-In the web interface, click \texttt{Query}. In the \texttt{Shortcuts}
-section, click the wrench (on the right side).
-
-In the drop-down menu, click \texttt{Delete}.
-\begin{figure}[h]
-\centering
-\includegraphics[width=.6\textwidth]{delete_shortcuts.png}
-\caption{Choosing which shortcuts to delete}
-\end{figure}
-
-Every user-defined shortcut (note: global shortcuts are not deletable in
-the webinterface at all) will receive checkbox and \texttt{Delete} and
-\texttt{Cancel} buttons appear at the bottom of the shortcuts section.
-
-Check all shortcuts which are to be deleted and click \texttt{Delete} or
-click \texttt{Cancel} to cancel the deletion.
-
-All deleted shortcuts are marked as deleted afterwards and will not
-appear again in the shortcuts section after reload.
-
-\subsection{Basic Shortcut}\label{basic-shortcut}
-
-The \texttt{Description} is a verbose definition of the query,
-e.g.~``Search for experiments and return a table.''. It will be the text
-that is visible in the shortcuts section.
-
-The \texttt{Query} is the query that will be executed with the shortcut.
-It adheres to the definition of the CaosDB Query Language (CQL).
-
-The corresponding query of our example is
-\texttt{SELECT\ date,\ name\ FROM\ Experiment}.
-
-\subsection{Advanced Shortcut}\label{advanced-shortcut}
-
-The basic shortcut does not allow for any parameterization. It is just a
-plain string or like a bookmark.
-
-Advanced shortcuts use a special syntax, where text placeholders are
-used to define parameters of the shortcut. The parameters can be set by
-the user at the time of the execution. An example can best illustrate
-what that means:
-
-Suppose you want to search for experiments by their year. The query for
-that would be
-\texttt{SELECT\ date,\ name\ FROM\ Experiment\ WITH\ date\ IN\ 2018}.
-
-Now, the actual year in the query can be made editable by replacing the
-year \texttt{2018} with \texttt{\{year\}}.
-
-The \texttt{Description} now must also contain this placeholder
-\texttt{\{year\}}, e.g.~``Search for experiements conducted in year
-\{year\}''. When the shortcut is displayed in the shortcuts section
-below the query input field, the placeholder is replaced by a text input
-field and the user can insert a year and execute the shortcut with the
-year being inserted into the query.
-
-\subsubsection{Placeholders}\label{placeholders}
-
-The placeholders have simple rules. A placeholder always starts and ends
-with curly brackets, like in the example \texttt{\{year\}}. The text
-inside the brackets (the placeholder's \emph{id}) may contain any
-combination of alphanumeric signs (0-9,a-z,A-Z). The use of special
-characters like colons, commas or the like is discouraged. They are
-reserved for future extensions of the placeholders. Apart from that, you
-are free to choose any placeholder \emph{id} that seems suitable for
-you.
-
-Both components of the query shortcut (description and query) must
-contain the same set of placeholders, otherwise the query shortcuts
-might not work as intended. If there is a \texttt{\{year\}} in the
-query, there must be a \texttt{\{year\}} in the description.
-
-Each placeholder \emph{id} must occur only once in both components -- if
-you need to use two years in your shortcut you have to use
-\texttt{\{year1\}} and \texttt{\{year2\}} or any other combinations of
-placeholder \emph{ids}.
-\end{document}
diff --git a/libs/qrcode-1.4.4.zip b/libs/qrcode-1.4.4.zip
new file mode 100644
index 0000000000000000000000000000000000000000..ed1e0f854985cfcec8cd4f419fe27c195f39c22c
Binary files /dev/null and b/libs/qrcode-1.4.4.zip differ
diff --git a/misc/ext_cosmetics_test_data.py b/misc/ext_cosmetics_test_data.py
new file mode 100755
index 0000000000000000000000000000000000000000..786f46c2d6bcd1d55488a15e2c4f50085f331950
--- /dev/null
+++ b/misc/ext_cosmetics_test_data.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+
+import caosdb as db
+
+# clean
+old = db.execute_query("FIND Test*")
+if len(old):
+    old.delete()
+
+# data model
+datamodel = db.Container()
+datamodel.extend([
+    db.Property("TestProp", datatype=db.TEXT),
+    db.RecordType("TestRecordType"),
+])
+
+datamodel.insert()
+
+
+# test data
+testdata = db.Container()
+
+test_cases = [
+    "no link",
+    "https://example.com",
+    "https://example.com and http://example.com",
+    "this is text https://example.com",
+    "this is text https://example.com and this as well",
+    "this is text https://example.com and another linke https://example.com",
+    "this is text https://example.com and another linke https://example.com and more text",
+    ("this is a lot of text with links in it Lorem ipsum dolor sit amet, "
+     "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore "
+     "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud "
+     "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. "
+     "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum "
+     "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non "
+     "proident, sunt in culpa qui officia deserunt mollit anim id est "
+     "laborum.https://example.com and another linke https://example.com and "
+     "more text and here comes a very long link: "
+     "https://example.com/this/has/certainly/more/than/40/characters/just/count/if/you/dont/believe/it.html"),
+]
+for test_case in test_cases:
+    testdata.append(db.Record().add_parent("TestRecordType").add_property("TestProp",
+                                                                          test_case))
+testdata.insert()
diff --git a/src/core/css/tour.css b/src/core/css/tour.css
index beec314ff8f8aa944e1fbb1fd9efd20e1fb17aa5..d772c79e8e9684690796b8acbd35b2116adf6f9b 100644
--- a/src/core/css/tour.css
+++ b/src/core/css/tour.css
@@ -251,6 +251,10 @@ div.caosdb-v-tour-toc-show {
     border: none;
 }
 
+.caosdb-v-tour-toc-header {
+    margin-left: 0.75rem;
+}
+
 /* For elements in popovers which are not for clicking but only illustrative. */
 .caosdb-v-tour-unclickable {
     cursor: text !important;
diff --git a/src/core/js/ext_bookmarks.js b/src/core/js/ext_bookmarks.js
index d99ff362d8a26ee4818a76a624c80f55f757c419..d9da463699ff7021b9f94a877aa3d9379dffb5dc 100644
--- a/src/core/js/ext_bookmarks.js
+++ b/src/core/js/ext_bookmarks.js
@@ -2,18 +2,19 @@
  * ** header v3.0
  * This file is a part of the CaosDB Project.
  *
- * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020,2021 IndiScale GmbH <info@indiscale.com>
  * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@indiscale.com>
  *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
  *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
  *
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -231,7 +232,7 @@ var ext_bookmarks = function ($, logger, config) {
      */
     const get_export_table = async function (bookmarks, preamble, tab, newline, leading_comments) {
         // TODO merge with related code in the module "caosdb_table_export".
-        preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8,": preamble);
+        preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8," : preamble);
         tab = tab || "%09";
         newline = newline || "%0A";
         leading_comments = (leading_comments ? leading_comments.join(newline) + newline : "");
@@ -489,10 +490,8 @@ var ext_bookmarks = function ($, logger, config) {
         counter = 0;
         update_collection([]);
 
-        // reset all buttons
-        get_bookmark_buttons().forEach((x) => {
-            set_button_state(x, false);
-        });
+        // re-init to reset all buttons
+        init();
 
         const storage_key_prefix = get_collection_prefix()
         remove_from_storage_by_prefix(storage_key_prefix);
@@ -566,6 +565,78 @@ var ext_bookmarks = function ($, logger, config) {
         collection_id = id;
     }
 
+    /**
+     * Add a button to add all query results to bookmarks.
+     */
+    const add_add_query_results_button = function () {
+        const row_id = "caosdb-f-add-query-to-bookmarks-row"
+        // do nothing if already existing
+        if ($("#" + row_id).length > 0) {
+            return;
+        }
+
+        // do nothing if no results
+        if ($(".caosdb-query-response-results").text().trim() == "0") {
+            return;
+        }
+
+        const button_html = $(`<div class="text-end" id=${row_id}>
+        <button class="btn btn-link" onclick="ext_bookmarks.add_query_results_to_bookmarks();">Bookmark all query results</button>
+</div>`)[0];
+
+        // Add to query results box
+        $(".caosdb-query-response-heading").append(button_html);
+    }
+
+    /**
+     * Execute select query and add all new ids to bookmarks.
+     */
+    const add_query_results_to_bookmarks = async function () {
+
+        const query_string = get_query_from_response();
+        const waiting_notification = createWaitingNotification(
+            "Adding results to bookmarks. Please wait and do not reload the page.");
+        const bookmarks_row = $("#caosdb-f-add-query-to-bookmarks-row");
+        bookmarks_row.find("button").hide();
+        bookmarks_row.append(waiting_notification);
+        const resp = await query(query_string);
+        for (const eid of resp) {
+            bookmark_storage.setItem(get_key(getEntityID(eid)), getEntityID(eid));
+        }
+        // re-init for correct display of counter and entities on page
+        init();
+        removeAllWaitingNotifications(bookmarks_row);
+        bookmarks_row.find("button").prop("disabled", true).show();
+    }
+
+    /**
+     * Transform a given query it to a "SELECT ID FROM ..." query.
+     *
+     * @param {string} query_string
+     */
+    const get_select_id_query_string = function (query_string) {
+        const test_string = query_string.toLowerCase();
+        const select_string = "SELECT ID FROM ";
+
+        // Will only be called on valid query results, so don't have to check
+        // for invalid query strings.
+        if (test_string.startsWith("find") || test_string.startsWith("count")) {
+            return select_string + query_string.slice(query_string.indexOf(" ") + 1);
+        }
+        if (test_string.startsWith("select")) {
+            return select_string + query_string.slice(test_string.indexOf("from ") + 5);
+        }
+    }
+
+    /**
+     * Return the SELECT query created from the contents of the query response field
+     */
+    const get_query_from_response = function () {
+
+        const orig_query = $(".caosdb-f-query-response-string")[0].innerText;
+        return get_select_id_query_string(orig_query.trim());
+    }
+
     /**
      * Initialize this module.
      */
@@ -591,6 +662,10 @@ var ext_bookmarks = function ($, logger, config) {
                 init_bookmark_buttons(e.target);
             }, true);
         }
+
+        if ("${BUILD_MODULE_EXT_ADD_QUERY_TO_BOOKMARKS}" == "ENABLED") {
+            add_add_query_results_button();
+        }
     }
 
     /**
@@ -625,7 +700,6 @@ var ext_bookmarks = function ($, logger, config) {
         if (data_getters[data_key]) {
             uncached = (await data_getters[data_key](id))
         }
-
         // don't cache if getting the information is trivial or there are other
         // reasons why this is in the data_no_cache array.
         if (data_no_cache.indexOf(data_key) == -1) {
@@ -652,6 +726,9 @@ var ext_bookmarks = function ($, logger, config) {
         get_bookmark_buttons: get_bookmark_buttons,
         init_button: init_button,
         get_bookmark_data: get_bookmark_data,
+        get_select_id_query_string: get_select_id_query_string,
+        get_query_from_response: get_query_from_response,
+        add_query_results_to_bookmarks: add_query_results_to_bookmarks,
     }
 };
 
@@ -666,10 +743,10 @@ $(document).ready(function () {
         // from the server.
         const get_path = async function (id) {
             if (id.indexOf("@") > -1) {
-              const entity = $(`[data-bmval='${id}']`);
-              if (entity.length > 0) {
-                  return getEntityPath(entity[0]) || "";
-              }
+                const entity = $(`[data-bmval='${id}']`);
+                if (entity.length > 0) {
+                    return getEntityPath(entity[0]) || "";
+                }
             }
             return $(await transaction.retrieveEntityById(id)).attr("path");
         }
@@ -681,20 +758,20 @@ $(document).ready(function () {
 
         const get_name = async function (id) {
             if (id.indexOf("@") > -1) {
-              const entity = $(`[data-bmval='${id}']`);
-              if (entity.length > 0) {
-                  return getEntityName(entity[0]) || "";
-              }
+                const entity = $(`[data-bmval='${id}']`);
+                if (entity.length > 0) {
+                    return getEntityName(entity[0]) || "";
+                }
             }
             return $(await transaction.retrieveEntityById(id)).attr("name");
         }
 
         const get_rt = async function (id) {
             if (id.indexOf("@") > -1) {
-              const entity = $(`[data-bmval='${id}']`);
-              if (entity.length > 0) {
-                  return getParents(entity[0]).join("/");
-              }
+                const entity = $(`[data-bmval='${id}']`);
+                if (entity.length > 0) {
+                    return getParents(entity[0]).join("/");
+                }
             }
             const parent_names = $(await transaction.retrieveEntityById(id))
                 .find("Parent").toArray().map(x => x.getAttribute("name"))
@@ -727,4 +804,4 @@ $(document).ready(function () {
         ext_bookmarks = ext_bookmarks($, log.getLogger("ext_bookmarks"), config);
         caosdb_modules.register(ext_bookmarks);
     }
-});
+});
\ No newline at end of file
diff --git a/src/core/js/ext_cosmetics.js b/src/core/js/ext_cosmetics.js
index 4d935a2a5afecd699fee1a24416c06b30d1adc46..f4f281123b39a87b7ef6848db4e84a81b5e30d9c 100644
--- a/src/core/js/ext_cosmetics.js
+++ b/src/core/js/ext_cosmetics.js
@@ -28,22 +28,33 @@
  */
 var cosmetics = new function () {
 
+    /**
+     * Cut-off length of links. When linkify processes the links any href
+     * longer than this will be cut off at character 25 and "[...]" will be
+     * appended for the link text.
+     */
+    var _link_cut_off_length = 40;
+
     var _linkify = function () {
         $('.caosdb-f-property-text-value').each(function (index) {
-            // TODO also extract and convert links surrounded by other text
-            if (/^https?:\/\//.test(this.innerText)) {
-                var uri = this.innerText;
-                var text = uri
+            if (/https?:\/\//.test(this.innerText)) {
+                var result = this.innerText.replace(/https?:\/\/[^\s]*/g, function (href, index) {
+                    var link_text = href;
+                    if (_link_cut_off_length > 4 && link_text.length > _link_cut_off_length) {
+                        link_text = link_text.substring(0, _link_cut_off_length - 5) + "[...]";
+                    }
+
+                    return `<a title="Open ${href} in a new tab." target="_blank" class="caosdb-v-property-href-value" href="${href}">${link_text} <i class="bi bi-box-arrow-up-right"></i></a>`;
+                });
 
-                $(this).parent().css("overflow", "hidden");
-                $(this).parent().css("text-overflow", "ellipsis");
-                $(this).html(`<a class="caosdb-v-property-href-value" href="${uri}">${text} <i class="bi bi-box-arrow-up-right"></i></a>`);
+                $(this).html(result);
             }
         });
     }
 
     /**
-     * Convert any text-value beginning with 'http(s)://' into a link.
+     * Convert any substring of a text-value beginning with 'http(s)://' into a
+     * link.
      *
      * A listener detects edit-mode changes and previews
      */
@@ -57,6 +68,7 @@ var cosmetics = new function () {
     }
 
     this.init = function () {
+        this.linkify = linkify;
         if ("${BUILD_MODULE_EXT_COSMETICS_LINKIFY}" == "ENABLED") {
             linkify();
         }
diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js
index 719d4f77943fdfca562961838dc02d95d5fd5cb1..c20cdefc2e9de73a21db81e3a7d5ebacebe73416 100644
--- a/src/core/js/ext_map.js
+++ b/src/core/js/ext_map.js
@@ -1470,7 +1470,7 @@ var caosdb_map = new function () {
          */
         this.make_entity_name_label = function (entity) {
             const name = getEntityName(entity);
-            const id = getEntityId(entity);
+            const id = getEntityID(entity);
 
             const entity_on_page = $(`#${id}`).length > 0;
             const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}`
diff --git a/src/core/js/ext_qrcode.js b/src/core/js/ext_qrcode.js
new file mode 100644
index 0000000000000000000000000000000000000000..d075ef884a89d407cb1e79b98f2045c6d4e25a26
--- /dev/null
+++ b/src/core/js/ext_qrcode.js
@@ -0,0 +1,201 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+/**
+ * Adds QR-Code generation to entities.
+ *
+ * @author Timm Fitschen
+ */
+var ext_qrcode = function ($, connection, getEntityVersion, getEntityID, QRCode, logger) {
+
+    const _buttons_list_class = "caosdb-v-entity-header-buttons-list";
+    const _qrcode_button_class = "caosdb-f-entity-qrcode-button";
+    const _qrcode_canvas_container = "caosdb-f-entity-qrcode";
+    const _qrcode_link_container = "caosdb-f-entity-qrcode-link";
+    const _qrcode_icon = `<i class="bi bi-upc"></i>`;
+
+    /**
+     * Create a new QR Code and a caption with a link, either linking to the
+     * entity head or to the exact version of the entity, based on the selected
+     * radio buttons and insert it into the modal.
+     *
+     * @param {HTMLElement} modal
+     * @param {string} entity_id
+     * @param {string} entity_version
+     */
+    var update_qrcode = function (modal, entity_id, entity_version) {
+        modal = $(modal);
+        const uri = modal.find("input[name=entity-qrcode-versioned]:checked").val();
+        var display_version = "";
+        if (uri.indexOf("@") > -1) {
+            display_version = `@${entity_version.substring(0,8)}`;
+        }
+        const description = `Entity <a href="${uri}">${entity_id}${display_version}</a>`;
+        modal.find(`.${_qrcode_canvas_container}`).empty();
+        modal.find(`.${_qrcode_link_container}`).empty().append(description);
+        QRCode.toCanvas(uri, {
+            "scale": 6
+        }).then((canvas) => {
+            modal.find(`.${_qrcode_canvas_container}`).empty().append(canvas);
+        }).catch(logger.error);
+    }
+
+    /**
+     * Create modal which shows the QR Code and a form where the user can choose
+     * whether the QR Code links to the entity head or the exact version of the
+     * entity.
+     *
+     * @param {string} modal_id - id of the resulting HTMLElement
+     * @param {string} entity_id
+     * @param {string} entity_version
+     * @return {HTMLElement} the resulting modal.
+     */
+    var create_qrcode_modal = function (modal_id, entity_id, entity_version) {
+        const uri = `${connection.getEntityUri([entity_id])}`;
+        const short_version = entity_version.substring(0, 8);
+        const modal = $(`<div class="modal fade" id="${modal_id}" tabindex="-1" aria-labelledby="${modal_id}-label" aria-hidden="true">
+      <div class="modal-dialog">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h4 class="modal-title" id="${modal_id}-label">QR Code</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+          </div>
+          <div class="modal-body text-center">
+            <div class="${_qrcode_canvas_container}"></div>
+            <div class="${_qrcode_link_container}"></div>
+          </div>
+          <div class="modal-footer justify-content-start">
+            <form>
+            <div class="form-check">
+              <label class="form-check-label">
+                <input value="${uri}" class="form-check-input" type="radio" name="entity-qrcode-versioned" checked>
+                Link to this entity.
+              </label>
+            </div>
+            <div class="form-check">
+              <label class="form-check-label" for="flexRadioDefault1">
+                <input value="${uri}@${entity_version}" class="form-check-input" type="radio" name="entity-qrcode-versioned">
+                Link to this exact version of this entity.
+              </label>
+            </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>`);
+        modal.find("form").change(() => {
+            update_qrcode(modal, entity_id, entity_version);
+        });
+        return modal[0];
+    }
+
+    /**
+     * Click handler of the QR Code button. The click event opens a modal showing
+     * the QR Code and a form where the user can choose whether the QR Code links
+     * to the entity head or the exact version of the entity.
+     *
+     * @param {string} entity_id
+     * @param {string} entity_version
+     */
+    var qrcode_button_click_handler = function (entity_id, entity_version) {
+        const modal_id = `qrcode-modal-${entity_id}-${entity_version}`;
+        var modal_element = document.getElementById(modal_id);
+        if (modal_element) {
+            // toggle modal
+            const modal = bootstrap.Modal.getInstance(modal_element);
+            modal.toggle();
+        } else {
+            modal_element = create_qrcode_modal(modal_id, entity_id, entity_version);
+            update_qrcode(modal_element, entity_id, entity_version);
+            $("body").append(modal_element);
+            const options = {};
+            const modal = new bootstrap.Modal(modal_element, options);
+            modal.show();
+        }
+    }
+
+    /**
+     * Create a button which opens the QR Code modal on click.
+     *
+     * @param {string} entity_id
+     * @param {string} entity_version
+     * @return {HTMLElement} the newly created button.
+     */
+    var create_qrcode_button = function (entity_id, entity_version) {
+        const button = $(`<button title="Create QR Code" type="button" class="${_qrcode_button_class} caosdb-v-entity-qrcode-button btn">${_qrcode_icon}</button>`);
+        button.click(() => {
+            qrcode_button_click_handler(entity_id, entity_version);
+        });
+        return button[0];
+    }
+
+    /**
+     * Add a qrcode button to a given entity.
+     * @param {HTMLElement} entity
+     */
+    var add_qrcode_to_entity = function (entity) {
+        const entity_id = getEntityID(entity);
+        const entity_version = getEntityVersion(entity);
+
+        $(entity).find(`.${_buttons_list_class}`).append(create_qrcode_button(entity_id, entity_version));
+    }
+
+    var remove_qrcode_button = function (entity) {
+        $(entity).find(`.${_buttons_list_class} .${_qrcode_button_class}`).remove();
+    }
+
+    var _init = function () {
+        for (let entity of $(".caosdb-entity-panel")) {
+            remove_qrcode_button(entity);
+            add_qrcode_to_entity(entity);
+        }
+    }
+
+    /**
+     * Initialize this module and append a QR Code button to all entities panels on the page.
+     *
+     * Removes all respective buttons if present before adding a new one.
+     */
+    var init = function () {
+        _init();
+
+        // edit-mode-listener
+        document.body.addEventListener(edit_mode.end_edit.type, _init, true);
+    };
+
+    return {
+        update_qrcode: update_qrcode,
+        add_qrcode_to_entity: add_qrcode_to_entity,
+        remove_qrcode_button: remove_qrcode_button,
+        create_qrcode_button: create_qrcode_button,
+        create_qrcode_modal: create_qrcode_modal,
+        qrcode_button_click_handler: qrcode_button_click_handler,
+        init: init
+    };
+
+}($, connection, getEntityVersion, getEntityID, QRCode, console);
+
+$(document).ready(function () {
+    if ("${BUILD_MODULE_EXT_QRCODE}" == "ENABLED") {
+        caosdb_modules.register(ext_qrcode);
+    }
+});
\ No newline at end of file
diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js
index 7cd597e128e8c09da9134f42f542898fb84a4e53..fe4d618c752490400e501116470cce0f28a909ad 100644
--- a/src/core/js/ext_references.js
+++ b/src/core/js/ext_references.js
@@ -43,13 +43,13 @@ var isOutOfViewport = function (elem) {
     out.top = bounding.top < 0;
     out.left = bounding.left < 0;
     out.bottom = bounding.bottom > (window.innerHeight ||
-        document.documentElement.clientHeight);
+  document.documentElement.clientHeight);
     out.right = bounding.right >
-        (window.innerWidth || document.documentElement.clientWidth);
+  (window.innerWidth || document.documentElement.clientWidth);
     out.any =
-        out.top || out.left || out.bottom || out.right;
+  out.top || out.left || out.bottom || out.right;
     out.all = out.top &&
-        out.left && out.bottom && out.right;
+  out.left && out.bottom && out.right;
     return out;
 };
 
@@ -90,56 +90,56 @@ var reference_list_summary = new function () {
      * array.
      */
     this.simplify_integer_numbers = function (array) {
-        logger.trace("enter simplify_integer_numbers", array);
-        var set = Array.from(new Set(array));
-
-        if (set.length === 0) {
-            return ""
-        } else if (set.length === 1) {
-            return `${set[0]}`;
-        }
-
-        // sort numerically
-        set.sort((a, b) => a - b);
-
-        if (set.length === 2) {
-            return `${set[0]}, ${set[1]}`;
-        }
-
-
-        var ret = `${set[0]}`;
-        var last = undefined;
-        // set[0];
-
-        // e.g. [1,2,3,4,5,8,9,10];
-        for (const next of set) {
-            // append '-' to summarize consecutive numbers
-            if (next - last === 1 && !ret.endsWith("-")) {
-                ret += "-";
-            }
-
-            if (next - last > 1) {
-
-                if (ret.endsWith("-")) {
-                    // close previous interval and start new
-                    ret += `${last}, ${next}`;
-                } else {
-                    // no previous interval, start interval.
-                    ret += `, ${next}`;
-                }
-            } else if (next === set[set.length - 1]) {
-                // finish interval if next is last item
-                ret += next;
-                break;
-            }
+  logger.trace("enter simplify_integer_numbers", array);
+  var set = Array.from(new Set(array));
+
+  if (set.length === 0) {
+      return ""
+  } else if (set.length === 1) {
+      return `${set[0]}`;
+  }
+
+  // sort numerically
+  set.sort((a, b) => a - b);
+
+  if (set.length === 2) {
+      return `${set[0]}, ${set[1]}`;
+  }
+
+
+  var ret = `${set[0]}`;
+  var last = undefined;
+  // set[0];
+
+  // e.g. [1,2,3,4,5,8,9,10];
+  for (const next of set) {
+      // append '-' to summarize consecutive numbers
+      if (next - last === 1 && !ret.endsWith("-")) {
+    ret += "-";
+      }
+
+      if (next - last > 1) {
+
+    if (ret.endsWith("-")) {
+        // close previous interval and start new
+        ret += `${last}, ${next}`;
+    } else {
+        // no previous interval, start interval.
+        ret += `, ${next}`;
+    }
+      } else if (next === set[set.length - 1]) {
+    // finish interval if next is last item
+    ret += next;
+    break;
+      }
 
 
-            last = next;
+      last = next;
 
-        }
+  }
 
-        // e.g. "1-5, 8-10"
-        return ret;
+  // e.g. "1-5, 8-10"
+  return ret;
     }
 
     /**
@@ -158,19 +158,19 @@ var reference_list_summary = new function () {
      * @return {HTMLElement|string} generated summary
      */
     this.generate = function (ref_infos, summary_container) {
-        logger.trace("enter generate", ref_infos);
-        if (ref_infos.length > 0 &&
-            typeof ref_infos[0].callback === "function") {
-            const summary =
-                ref_infos[0].callback(ref_infos);
-            if (summary && summary_container) {
-                $(summary_container).append(summary);
-            }
-            logger.trace("leave generate", summary);
-            return summary;
-        }
-        logger.trace("leave generate, return undefined");
-        return undefined;
+  logger.trace("enter generate", ref_infos);
+  if (ref_infos.length > 0 &&
+      typeof ref_infos[0].callback === "function") {
+      const summary =
+    ref_infos[0].callback(ref_infos);
+      if (summary && summary_container) {
+    $(summary_container).append(summary);
+      }
+      logger.trace("leave generate", summary);
+      return summary;
+  }
+  logger.trace("leave generate, return undefined");
+  return undefined;
     }
 }
 
@@ -205,12 +205,12 @@ var resolve_references = new function () {
      * last scroll event.
      */
     var scroll_listener = () => {
-        if (_scroll_timeout) {
-            clearTimeout(_scroll_timeout);
-        }
-        _scroll_timeout = setTimeout(function () {
-            resolve_references.update_visible_references();
-        }, 500);
+  if (_scroll_timeout) {
+      clearTimeout(_scroll_timeout);
+  }
+  _scroll_timeout = setTimeout(function () {
+      resolve_references.update_visible_references();
+  }, 500);
     };
 
 
@@ -220,15 +220,15 @@ var resolve_references = new function () {
      * visible references.
      */
     this.init = function () {
-        if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
-            scroll_listener();
+  if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
+      scroll_listener();
 
-            // mainly for vertical scrolling
-            $(window).scroll(scroll_listener);
+      // mainly for vertical scrolling
+      $(window).scroll(scroll_listener);
 
-            // for horizontal scrolling.
-            $(".caosdb-value-list").scroll(scroll_listener);
-        }
+      // for horizontal scrolling.
+      $(".caosdb-value-list").scroll(scroll_listener);
+  }
     }
 
     /**
@@ -241,9 +241,9 @@ var resolve_references = new function () {
      *
      */
     this.is_in_viewport_vertically = function (elem) {
-        var out =
-            isOutOfViewport(elem);
-        return !(out.top || out.bottom);
+  var out =
+      isOutOfViewport(elem);
+  return !(out.top || out.bottom);
     }
 
     /** Check if an element is inside of the viewport on the horizontal axis.
@@ -257,35 +257,19 @@ var resolve_references = new function () {
      *
      */
     this.is_in_viewport_horizontally = function (elem) {
-        var scrollbox = elem.parentElement.parentElement;
-        // Check this condition only if the grand parent is a list and return true
-        // otherwise.
-        if (scrollbox.classList.contains("caosdb-value-list") ==
-            true) {
-            var boundel = elem.getBoundingClientRect();
-            var boundscroll = scrollbox.getBoundingClientRect();
-            var leftcrit = boundel.right > boundscroll.left;
-            var rightcrit = boundel.left < boundscroll.right;
-            return leftcrit && rightcrit;
-        } else {
-            return true;
-        }
-    }
-
-
-    /**
-     * Return the name of a person as firstname + lastname
-     */
-    this.get_person_str = function (el) {
-        var valpr = getProperties(el);
-        if (valpr == undefined) {
-            return;
-        }
-        return valpr.filter(valprel =>
-                valprel.name.toLowerCase() == "firstname")[0].value +
-            " " +
-            valpr.filter(valprel => valprel.name.toLowerCase() ==
-                "lastname")[0].value;
+  var scrollbox = elem.parentElement.parentElement;
+  // Check this condition only if the grand parent is a list and return true
+  // otherwise.
+  if (scrollbox.classList.contains("caosdb-value-list") ==
+      true) {
+      var boundel = elem.getBoundingClientRect();
+      var boundscroll = scrollbox.getBoundingClientRect();
+      var leftcrit = boundel.right > boundscroll.left;
+      var rightcrit = boundel.left < boundscroll.right;
+      return leftcrit && rightcrit;
+  } else {
+      return true;
+  }
     }
 
 
@@ -296,13 +280,13 @@ var resolve_references = new function () {
      * {string} par - parent name.  @return {boolean}
      */
     this.is_child = function (entity, par) {
-        var pars = resolve_references.getParents(entity);
-        for (const thispar of pars) {
-            if (thispar.name === par) {
-                return true;
-            }
-        }
-        return false;
+  var pars = resolve_references.getParents(entity);
+  for (const thispar of pars) {
+      if (thispar.name === par) {
+    return true;
+      }
+  }
+  return false;
     }
 
     /**
@@ -318,46 +302,56 @@ var resolve_references = new function () {
     /**
      * Return a reference_info for an entity.
      *
+     * You may add your own custom resolver by specifying a JS module
+     * via the `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER` build
+     * variable. The custom resolver has to be a JS module (typically
+     * located at caosdb-webui/src/ext/js), the name of which is given
+     * as the value of `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER`. It has
+     * to provide a `resolve` function that takes the entity id to be
+     * resolved as a string and returns a `reference_info` object with
+     * the resolved custom reference as a `text` property.
+     *
+     * See caosdb-webui/src/ext/js/person_reference_resolver.js for an
+     * example.
+     *
      * TODO refactor to be configurable.  @async @param {string} id - the id of
      * the entity which is to be resolved.  @return {reference_info}
      */
     this.resolve_reference = async function (id) {
-        const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"];
-        if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") {
-            // try custom_reference_resolver and fall-back to standard implementation
-            var ret = await custom_reference_resolver.resolve(id);
-            if (ret) {
-              return ret;
-            }
-        }
-
-        const entity = (await resolve_references.retrieve(id))[0];
-
-        // TODO handle multiple parents
-        const par = resolve_references.getParents(entity)[0] || {};
-
-        var ret = {
-            "text": id
-        };
-        if (getEntityHeadingAttribute(entity, "path") !==
-            undefined || par.name == "Image") {
-            // show file name
-            var pths = getEntityHeadingAttribute(entity, "path")
-                .split("/");
-            ret["text"] = pths[pths.length - 1];
-        } else if (par.name === "Person") {
-            ret["text"] = this.get_person_str(entity);
-        } else if (par.name === "TestReferenced" && typeof resolve_references.test_resolver === "function") {
-            // this is a test case, initialized by the test suite.
-            ret = resolve_references.test_resolver(entity);
-        } else {
-            var name = getEntityName(entity);
-            if (typeof name !== "undefined" && name.length > 0) {
-                ret["text"] = name;
-            }
-        }
-
+  const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"];
+  if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") {
+      // try custom_reference_resolver and fall-back to standard implementation
+      var ret = await custom_reference_resolver.resolve(id);
+      if (ret) {
         return ret;
+      }
+  }
+
+  const entity = (await resolve_references.retrieve(id))[0];
+
+  // TODO handle multiple parents
+  const par = resolve_references.getParents(entity)[0] || {};
+
+  var ret = {
+      "text": id
+  };
+  if (getEntityHeadingAttribute(entity, "path") !==
+      undefined || par.name == "Image") {
+      // show file name
+      var pths = getEntityHeadingAttribute(entity, "path")
+    .split("/");
+      ret["text"] = pths[pths.length - 1];
+  } else if (par.name === "TestReferenced" && typeof resolve_references.test_resolver === "function") {
+      // this is a test case, initialized by the test suite.
+      ret = resolve_references.test_resolver(entity);
+  } else {
+      var name = getEntityName(entity);
+      if (typeof name !== "undefined" && name.length > 0) {
+    ret["text"] = name;
+      }
+  }
+
+  return ret;
     }
 
 
@@ -372,12 +366,12 @@ var resolve_references = new function () {
      * @return {HTMLElement} the new/existing target element.
      */
     this.add_target = function (element) {
-        if(element.getElementsByClassName(this._target_class).length > 0){
-            return element.getElementsByClassName(this._target_class);
-        } else {
-            return $(`<span class="${this._target_class}"/>`)
-                .appendTo(element)[0];
-        }
+  if(element.getElementsByClassName(this._target_class).length > 0){
+      return element.getElementsByClassName(this._target_class);
+  } else {
+      return $(`<span class="${this._target_class}"/>`)
+    .appendTo(element)[0];
+  }
     }
 
     /**
@@ -388,14 +382,14 @@ var resolve_references = new function () {
      * @return {reference_info} the resolved reference information
      */
     this.update_single_resolvable_reference = async function (rs) {
-        $(rs).find(".caosdb-id-button").hide();
-        const target = resolve_references.add_target(rs);
-        const id = getEntityID(rs);
-        target.textContent = id;
-        const resolved_entity_info = (
-            await resolve_references.resolve_reference(id));
-        target.textContent = resolved_entity_info.text;
-        return resolved_entity_info;
+  $(rs).find(".caosdb-id-button").hide();
+  const target = resolve_references.add_target(rs);
+  const id = getEntityID(rs);
+  target.textContent = id;
+  const resolved_entity_info = (
+      await resolve_references.resolve_reference(id));
+  target.textContent = resolved_entity_info.text;
+  return resolved_entity_info;
     }
 
 
@@ -411,10 +405,10 @@ var resolve_references = new function () {
      * @return {HTMLElement} a summary field.
      */
     this.add_summary_field = function (list_values) {
-        const summary = $(
-            `<div class="${resolve_references._summary_class}"/>`);
-        $(list_values).prepend(summary);
-        return summary[0];
+  const summary = $(
+      `<div class="${resolve_references._summary_class}"/>`);
+  $(list_values).prepend(summary);
+  return summary[0];
     }
 
     this._summary_class = "caosdb-resolve-reference-summary";
@@ -426,9 +420,9 @@ var resolve_references = new function () {
     this._unresolved_class_name = "caosdb-resolvable-reference";
 
     this.get_resolvable_properties = function (container) {
-        const _unresolved_class_name = this._unresolved_class_name;
-        return $(container).find(".caosdb-f-property-value").has(
-            `.${_unresolved_class_name}`).toArray();
+  const _unresolved_class_name = this._unresolved_class_name;
+  return $(container).find(".caosdb-f-property-value").has(
+      `.${_unresolved_class_name}`).toArray();
     }
 
 
@@ -442,115 +436,115 @@ var resolve_references = new function () {
      * @param {HTMLElement} container
      */
     this.update_visible_references = async function (container) {
-        const property_values = resolve_references
-            .get_resolvable_properties(container || document.body);
-
-        const _unresolved_class_name = resolve_references
-            ._unresolved_class_name;
-
-        // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references.
-        for (const property_value of property_values) {
-            var lists = findElementByConditions(
-                property_value, 
-                x => x.classList.contains("caosdb-value-list"), 
-                x => x.classList.contains("caosdb-preview-container"))
-            lists = $(lists).has(`.${_unresolved_class_name}`);
-
-            if (lists.length > 0) {
-                logger.debug("processing list of references", lists);
-
-                for (var i = 0; i < lists.length; i++) {
-                    const list = lists[i];
-                    if (resolve_references
-                            .is_in_viewport_vertically(list)) {
-                        const rs = $(list).find(
-                                `.${_unresolved_class_name}`)
-                            .toggleClass(_unresolved_class_name, false);
-
-                        // First resolve only one reference. If the `ref_info`
-                        // indicates that a summary is to be generated from the
-                        // list of references, retrieve all other other
-                        // references. Otherwise retrieve only those which are
-                        // visible in the viewport horizontally and trigger the
-                        // retrieval of the others when they are scrolled into
-                        // the view port.
-                        const first_ref_info = await resolve_references
-                            .update_single_resolvable_reference(rs[0]);
-
-                        first_ref_info["index"] = 0;
-
-                        if (typeof first_ref_info.callback === "function") {
-                            // there is a callback function, hence we need to
-                            // generate a summary.
-                            logger.debug("loading all references for summary",
-                                rs);
-                            const summary_field = resolve_references
-                                .add_summary_field(property_value);
-
-                            // collect ref infos for the summary
-                            const ref_infos = [first_ref_info];
-                            for (var j = 1; j < rs.length; j++) {
-                                const ref_info = resolve_references
-                                    .update_single_resolvable_reference(rs[j]);
-                                ref_info["index"] = j;
-                                ref_infos.push(ref_info);
-                            }
-
-                            // wait for resolution of references,
-                            // then generate the summary,
-                            // dispatch event when ready.
-                            Promise.all(ref_infos)
-                                .then(_ref_infos => {reference_list_summary
-                                    .generate(_ref_infos, summary_field);})
-                                .then(() => {
-                                    summary_field.dispatchEvent(
-                                        resolve_references
-                                            .summary_ready_event
-                                    );})
-                                .catch((err) => {
-                                    logger.error(err);
-                                })
-
-                        } else {
-                            // no summary to be generated
-
-                            logger.debug("lazy loading references", rs);
-                            for (var j = 1; j < rs.length; j++) {
-                                // mark others to be loaded later and only if
-                                // visible
-                                $(rs[j]).toggleClass(_unresolved_class_name, true);
-                            }
-                        }
-                    }
-                }
-            }
-
-            // Load all remaining references. These are single reference values
-            // and those references from lists which are left for lazy loading.
-            const rs = findElementByConditions(
-                property_value, 
-                x => x.classList.contains(`${_unresolved_class_name}`), 
-                x => x.classList.contains("caosdb-preview-container"));
-            for (var i = 0; i < rs.length; i++) {
-                if (resolve_references.is_in_viewport_vertically(
-                        rs[i]) &&
-                    resolve_references.is_in_viewport_horizontally(
-                        rs[i])) {
-                    logger.debug("processing single references", rs);
-                    $(rs[i]).toggleClass(_unresolved_class_name, false);
-
-                    // discard return value as it is not needed for any summary
-                    // generation as above.
-                    resolve_references.update_single_resolvable_reference(rs[i]);
-                }
-            }
+  const property_values = resolve_references
+      .get_resolvable_properties(container || document.body);
+
+  const _unresolved_class_name = resolve_references
+      ._unresolved_class_name;
+
+  // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references.
+  for (const property_value of property_values) {
+      var lists = findElementByConditions(
+    property_value,
+    x => x.classList.contains("caosdb-value-list"),
+    x => x.classList.contains("caosdb-preview-container"))
+      lists = $(lists).has(`.${_unresolved_class_name}`);
+
+      if (lists.length > 0) {
+    logger.debug("processing list of references", lists);
+
+    for (var i = 0; i < lists.length; i++) {
+        const list = lists[i];
+        if (resolve_references
+          .is_in_viewport_vertically(list)) {
+      const rs = $(list).find(
+        `.${_unresolved_class_name}`)
+          .toggleClass(_unresolved_class_name, false);
+
+      // First resolve only one reference. If the `ref_info`
+      // indicates that a summary is to be generated from the
+      // list of references, retrieve all other other
+      // references. Otherwise retrieve only those which are
+      // visible in the viewport horizontally and trigger the
+      // retrieval of the others when they are scrolled into
+      // the view port.
+      const first_ref_info = await resolve_references
+          .update_single_resolvable_reference(rs[0]);
+
+      first_ref_info["index"] = 0;
+
+      if (typeof first_ref_info.callback === "function") {
+          // there is a callback function, hence we need to
+          // generate a summary.
+          logger.debug("loading all references for summary",
+        rs);
+          const summary_field = resolve_references
+        .add_summary_field(property_value);
+
+          // collect ref infos for the summary
+          const ref_infos = [first_ref_info];
+          for (var j = 1; j < rs.length; j++) {
+        const ref_info = resolve_references
+            .update_single_resolvable_reference(rs[j]);
+        ref_info["index"] = j;
+        ref_infos.push(ref_info);
+          }
+
+          // wait for resolution of references,
+          // then generate the summary,
+          // dispatch event when ready.
+          Promise.all(ref_infos)
+        .then(_ref_infos => {reference_list_summary
+            .generate(_ref_infos, summary_field);})
+        .then(() => {
+            summary_field.dispatchEvent(
+          resolve_references
+              .summary_ready_event
+            );})
+        .catch((err) => {
+            logger.error(err);
+        })
+
+      } else {
+          // no summary to be generated
+
+          logger.debug("lazy loading references", rs);
+          for (var j = 1; j < rs.length; j++) {
+        // mark others to be loaded later and only if
+        // visible
+        $(rs[j]).toggleClass(_unresolved_class_name, true);
+          }
+      }
         }
     }
+      }
+
+      // Load all remaining references. These are single reference values
+      // and those references from lists which are left for lazy loading.
+      const rs = findElementByConditions(
+    property_value,
+    x => x.classList.contains(`${_unresolved_class_name}`),
+    x => x.classList.contains("caosdb-preview-container"));
+      for (var i = 0; i < rs.length; i++) {
+    if (resolve_references.is_in_viewport_vertically(
+      rs[i]) &&
+        resolve_references.is_in_viewport_horizontally(
+      rs[i])) {
+        logger.debug("processing single references", rs);
+        $(rs[i]).toggleClass(_unresolved_class_name, false);
+
+        // discard return value as it is not needed for any summary
+        // generation as above.
+        resolve_references.update_single_resolvable_reference(rs[i]);
+    }
+      }
+  }
+    }
 }
 
 
 $(document).ready(function () {
     if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
-        caosdb_modules.register(resolve_references);
+  caosdb_modules.register(resolve_references);
     }
 });
diff --git a/src/core/js/fileupload.js b/src/core/js/fileupload.js
index 31d86589286f1761f481cc9d9bb6a557a63cbce1..8a988e86a6a9b5bbb39f39a37d63b32b144c748d 100644
--- a/src/core/js/fileupload.js
+++ b/src/core/js/fileupload.js
@@ -156,7 +156,7 @@ var fileupload = new function() {
         // get property-value input element (in case of FILE property)
         var input = $(property).find(".caosdb-f-property-value input");
         var set_value = function(entity) {
-            input.val(getEntityId(entity));
+            input.val(getEntityID(entity));
         }
 
         if (input.length == 0) {
@@ -207,7 +207,7 @@ var fileupload = new function() {
                 getEntityName(entity) + `</code> has been uploaded.</div>`);
 
             input.after(`<a class="btn btn-secondary btn-sm"
-  href="` + connection.getEntityUri([getEntityId(entity)]) + `" target= "_blank">` +
+  href="` + connection.getEntityUri([getEntityID(entity)]) + `" target= "_blank">` +
                 getEntityName(entity) + `</a>`);
 
         };
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index 74dc62be15551f987707253ca201777f7c3929ac..efa28c9b39921df3e02a25ec95d4601f752fa288 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -164,19 +164,19 @@ this.navbar = new function () {
 
         // show form and hide the show_button
         const _in = () => {
-          // xs means viewport <= 768px
-          form.removeClass("d-none");
-          form.addClass("d-xs-inline-block");
-          show_button.removeClass("d-inline-block");
-          show_button.addClass("d-none");
+            // xs means viewport <= 768px
+            form.removeClass("d-none");
+            form.addClass("d-xs-inline-block");
+            show_button.removeClass("d-inline-block");
+            show_button.addClass("d-none");
         }
         // hide form and show the show_button
         const _out = () => {
-          // xs means viewport <= 768px
-          form.removeClass("d-xs-inline-block");
-          form.addClass("d-none");
-          show_button.removeClass("d-none");
-          show_button.addClass("d-inline-block");
+            // xs means viewport <= 768px
+            form.removeClass("d-xs-inline-block");
+            form.addClass("d-none");
+            show_button.removeClass("d-none");
+            show_button.addClass("d-inline-block");
         }
         show_button.on("click", () => {
             // show form...
@@ -607,7 +607,7 @@ this.transformation = new function () {
      * @return {XMLDocument} xslt script
      */
     this.retrieveEntityXsl = async function _rEX(root_template) {
-        const _root = root_template || '<xsl:template match="/" xmlns="http://www.w3.org/1999/xhtml"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>';
+        const _root = root_template || '<xsl:template match="/" xmlns="http://www.w3.org/1999/xhtml"><div class="root"><xsl:apply-templates select="Response/child::*" mode="entities"/></div></xsl:template>';
         var entityXsl = await transformation.retrieveXsltScript("entity.xsl");
         var commonXsl = await transformation.retrieveXsltScript("common.xsl");
         var errorXsl = await transformation.retrieveXsltScript('messages.xsl');
@@ -812,7 +812,7 @@ this.transaction = new function () {
                     $(updatePanel).insertBefore(entity);
                     // create and add waiting notification
                     updatePanel.appendChild(transaction.update.createWaitRetrieveNotification());
-                    let entityId = getEntityId(entity);
+                    let entityId = getEntityID(entity);
                     transaction.update.retrieveOldEntityXmlString(entityId).then(xmlstr => {
                         app.openForm(xmlstr);
                     }, err => {
@@ -1313,7 +1313,7 @@ var queryForm = new function () {
         var submithandler = function () {
             // store current query
             var queryField = form.query;
-            var value = queryField.value.toUpperCase();
+            var value = queryField.value.toUpperCase().trim();
             if (typeof value == "undefined" || value.length == 0) {
                 return;
             }
@@ -1327,7 +1327,7 @@ var queryForm = new function () {
                 paging = form.P.value
             }
 
-            queryForm.redirect(queryField.value, paging);
+            queryForm.redirect(queryField.value.trim(), paging);
         };
 
         $("#caosdb-query-textarea").on("keydown", (e) => {
@@ -1507,10 +1507,12 @@ function createErrorNotification(msg) {
  * Create a waiting notification with a informative message for the waiting user. 
  *
  * @param {String} info, a message for the user
+ * @param {String} id, optional, the id of the message div.  Default is empty
  * @return {HTMLElement} A div with class `caosdb-preview-waiting-notification`.
  */
-function createWaitingNotification(info) {
-    return $('<div class="' + globalClassNames.WaitingNotification + '">' + info + '</div>')[0];
+function createWaitingNotification(info, id) {
+    id = id ? `id="${id}"` : "";
+    return $(`<div class="${globalClassNames.WaitingNotification}" ${id}>${info}</div>`)[0];
 }
 
 /**
@@ -1520,23 +1522,10 @@ function createWaitingNotification(info) {
  * @return {HTMLElement} The parameter `elem`.
  */
 function removeAllWaitingNotifications(elem) {
-    $(elem.getElementsByClassName(globalClassNames.WaitingNotification)).remove();
+    $(elem).find(`.${globalClassNames.WaitingNotification}`).remove();
     return elem;
 }
 
-/**
- * Extract the ID of an entity by parsing the textContent of the first occuring element with
- * class `caosdb-id`.
- *
- * @param {HTMLElement} entity
- * @returns {Number} ID of entity.
- */
-function getEntityId(entity) {
-    let id = Number.parseInt(entity.getElementsByClassName("caosdb-id")[0].textContent);
-    if (isNaN(id)) throw new Error("id was NaN");
-    return id;
-}
-
 // TODO remove and use connection.post
 /**
  * Post an xml document to basepath/Entity
@@ -1938,4 +1927,4 @@ class _CaosDBModules {
 
 var caosdb_modules = new _CaosDBModules()
 
-$(document).ready(initOnDocumentReady);
+$(document).ready(initOnDocumentReady);
\ No newline at end of file
diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl
index d562bf99f7611b976e10d5349d0c99972d6d8fdd..92f08ea70645a8282f97f364c9fc4143f37afd6a 100644
--- a/src/core/xsl/entity.xsl
+++ b/src/core/xsl/entity.xsl
@@ -25,6 +25,10 @@
   <xsl:output method="html"/>
   <!-- These little colored Rs, RTs, Ps, and Fs which hilite the beginning 
         of a new entity. -->
+  <xsl:template match="Entity" mode="entity-heading-label">
+    <span class="badge caosdb-f-entity-role caosdb-label-entity me-1"
+    title="This is an entity. The role is not specified.">E</span>
+  </xsl:template>
   <xsl:template match="Property" mode="entity-heading-label">
     <span class="badge caosdb-f-entity-role caosdb-label-property me-1"
     data-entity-role="Property" title="This entity is a Property.">P</span>
@@ -98,7 +102,7 @@
     </div>
   </xsl:template>
   <!-- Main entry for ENTITIES -->
-  <xsl:template match="Property|Record|RecordType|File" mode="entities">
+  <xsl:template match="Property|Record|RecordType|File|Response/Entity" mode="entities">
     <div class="card caosdb-entity-panel mb-2">
       <xsl:apply-templates select="Version" mode="entity-version-marker"/>
       <xsl:attribute name="id">
diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl
index ee4df81be60558e6b6aa2e558096d7420636349f..529945d7c37a95a0687473dce34f0269e2942c92 100644
--- a/src/core/xsl/navbar.xsl
+++ b/src/core/xsl/navbar.xsl
@@ -135,7 +135,7 @@
                 <span id="caosdb-f-bookmarks-collection-counter" class="badge bg-secondary">0</span>
                   Bookmarks
                 </a>
-              <ul class="dropdown-menu dropdown-menu-light" aria-labelledby="navbarBookmarkMenuLink">
+              <ul class="dropdown-menu dropdown-menu-end dropdown-menu-light" aria-labelledby="navbarBookmarkMenuLink">
                 <li class="disabled" id="caosdb-f-bookmarks-collection-link"
                     title="Show all bookmarked entities.">
                   <a class="dropdown-item">Show all</a></li>
@@ -234,10 +234,10 @@
             <i class="bi-person-fill"></i>
             <span class="caret"></span>
           </a>
-          <ul class="dropdown-menu dropdown-menu-light">
+          <ul class="dropdown-menu dropdown-menu-end dropdown-menu-light">
             <xsl:if test="/Response/@realm='${BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM}'">
               <li>
-                <a title="Change your password." href="#" data-toggle="modal" data-target="#caosdb-f-change-password-form">Change Password</a>
+                <a class="dropdown-item" title="Change your password." href="#" data-bs-toggle="modal" data-bs-target="#caosdb-f-change-password-form">Change Password</a>
               </li>
             </xsl:if>
             <li>
diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl
index 2b647c07bebe7f7cd72198baf27e45318e25a18e..702a390f28ada96e140f40f10218b1740ab10700 100644
--- a/src/core/xsl/query.xsl
+++ b/src/core/xsl/query.xsl
@@ -56,7 +56,9 @@
           <div class="col-sm-10 caosdb-overflow-box">
             <div class="caosdb-overflow-content">
               <span>Query: </span>
-              <xsl:value-of select="@string"/>
+              <span class="caosdb-f-query-response-string">
+                <xsl:value-of select="@string"/>
+              </span>
             </div>
           </div>
           <div class="col-sm-2 text-end">
@@ -223,15 +225,28 @@
         <xsl:value-of select="$field-name"/>
       </xsl:attribute>
       <div class="caosdb-f-property-value caosdb-v-property-value">
-        <xsl:apply-templates select="/Response/*[@id=$entity-id and Version/@id=$version-id]" mode="walk-select-segments">
-        <!--<xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments">-->
-          <xsl:with-param name="first-segment">
-            <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/>
-          </xsl:with-param>
-          <xsl:with-param name="next-segments">
-            <xsl:value-of select="substring-after($field-name, '.')"/>
-          </xsl:with-param>
-        </xsl:apply-templates>
+        <xsl:choose>
+          <xsl:when test="$version-id!=''">
+            <xsl:apply-templates select="/Response/*[@id=$entity-id and Version/@id=$version-id]" mode="walk-select-segments">
+              <xsl:with-param name="first-segment">
+                <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/>
+              </xsl:with-param>
+              <xsl:with-param name="next-segments">
+                <xsl:value-of select="substring-after($field-name, '.')"/>
+              </xsl:with-param>
+            </xsl:apply-templates>
+          </xsl:when>
+          <xsl:otherwise>
+            <xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments">
+              <xsl:with-param name="first-segment">
+                <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/>
+              </xsl:with-param>
+              <xsl:with-param name="next-segments">
+                <xsl:value-of select="substring-after($field-name, '.')"/>
+              </xsl:with-param>
+            </xsl:apply-templates>
+          </xsl:otherwise>
+        </xsl:choose>
       </div>
     </td>
   </xsl:template>
diff --git a/doc/QueryShortcuts/choose_edit.png b/src/doc/extension/images/choose_edit.png
similarity index 100%
rename from doc/QueryShortcuts/choose_edit.png
rename to src/doc/extension/images/choose_edit.png
diff --git a/doc/QueryShortcuts/create_shortcut.png b/src/doc/extension/images/create_shortcut.png
similarity index 100%
rename from doc/QueryShortcuts/create_shortcut.png
rename to src/doc/extension/images/create_shortcut.png
diff --git a/doc/QueryShortcuts/create_success.png b/src/doc/extension/images/create_success.png
similarity index 100%
rename from doc/QueryShortcuts/create_success.png
rename to src/doc/extension/images/create_success.png
diff --git a/doc/QueryShortcuts/delete_shortcuts.png b/src/doc/extension/images/delete_shortcuts.png
similarity index 100%
rename from doc/QueryShortcuts/delete_shortcuts.png
rename to src/doc/extension/images/delete_shortcuts.png
diff --git a/doc/QueryShortcuts/delete_success.png b/src/doc/extension/images/delete_success.png
similarity index 100%
rename from doc/QueryShortcuts/delete_success.png
rename to src/doc/extension/images/delete_success.png
diff --git a/doc/QueryShortcuts/edit_shortcut.png b/src/doc/extension/images/edit_shortcut.png
similarity index 100%
rename from doc/QueryShortcuts/edit_shortcut.png
rename to src/doc/extension/images/edit_shortcut.png
diff --git a/doc/QueryShortcuts/edit_success.png b/src/doc/extension/images/edit_success.png
similarity index 100%
rename from doc/QueryShortcuts/edit_success.png
rename to src/doc/extension/images/edit_success.png
diff --git a/doc/QueryShortcuts/shortcut_toolbox.png b/src/doc/extension/images/shortcut_toolbox.png
similarity index 100%
rename from doc/QueryShortcuts/shortcut_toolbox.png
rename to src/doc/extension/images/shortcut_toolbox.png
diff --git a/src/doc/extension/query_templates.rst b/src/doc/extension/query_templates.rst
new file mode 100644
index 0000000000000000000000000000000000000000..015d26a21fe20bc09dc97db9c7a3e7a1ca58a0b3
--- /dev/null
+++ b/src/doc/extension/query_templates.rst
@@ -0,0 +1,216 @@
+Introduction
+============
+
+The WebUI supports the creation of query shortcuts which appear below
+the normal query input field. These shortcuts facilitate looking for
+data as query strings which are used frequently. They can be stored and
+reused.
+
+.. figure:: images/shortcut_toolbox.png
+   :alt: The Shortcuts in the Query Panel; Note the Toolbox for in the
+   top right
+
+   The Shortcuts in the Query Panel; Note the Toolbox for in the top
+   right
+
+There are two ways to integrate query templates into the WebUI:
+
+-  | Global shortcuts are integrated by the webmaster only. They are
+     defined and stored in a
+   | ``./conf/ext/json/global_query_shortcuts.json`` in the root
+     directory of the webui. (See the example below.)
+
+-  User-defined templates can be defined by users and are only visible
+   for the user who created them. In this sense, user-defined shortcuts
+   are also private, whereas global shortcuts are always publicly
+   visible.
+
+User-defined Query Shortcuts
+============================
+
+Create a New Shortcut
+---------------------
+
+New Query Shortcuts can be generated by any authenticated user with
+sufficient write permissions.
+
+In the web interface, click ``Query``. In the ``Shortcuts`` section,
+click the wrench (on the right side).
+
+In the drop-down menu, click ``Create``.
+
+It now opens a form with two input fields, ``Description`` and
+``Query``.
+
+.. figure:: images/create_shortcut.png
+   :alt: The view to create a new shortcut
+
+   The view to create a new shortcut
+
+See `2.4 <#basic-shortcut>`__ and `2.5 <#advanced-shortcut>`__ for
+further explanation of the components of a Query Shortcut.
+
+Edit the fields and click ``Submit`` for the creation of the new
+shortcut or click ``Cancel`` to cancel the process.
+
+The new shortcut is shown in the shortcuts section.
+
+.. figure:: images/create_success.png
+   :alt: The view when creation was successful
+
+   The view when creation was successful
+
+Change an Existing Shortcut
+---------------------------
+
+Existing Query Shortcuts which are visible in your shortcuts section can
+be edited directly in the shortcuts section.
+
+In the web interface, click ``Query``. In the ``Shortcuts`` section,
+click the wrench (on the right side).
+
+In the drop-down menu, click ``Edit``.
+
+.. figure:: images/choose_edit.png
+   :alt: Choosing which shortcut to edit
+
+   Choosing which shortcut to edit
+
+Every editable shortcut (note: global shortcuts are not editable in the
+webinterface at all) will receive a new button ``Edit``
+
+Click ``Edit`` of the shortcut that is to be changed.
+
+It now opens a form with two input fields, ``Description`` and
+``Query``, pre-filled.
+
+See `2.4 <#basic-shortcut>`__ and `2.5 <#advanced-shortcut>`__ for
+further explanation of the components of a Query Shortcut.
+
+Edit the fields and click ``Submit`` for the creation of the new
+shortcut or click ``Cancel`` to cancel the process.
+
+The updated shortcut is shown in the shortcuts section.
+
+See the
+
+Delete an Existing Shortcut
+---------------------------
+
+Existing Query Shortcut which are visible in your shortcuts section can
+be edited directly in the shortcuts section.
+
+In the web interface, click ``Query``. In the ``Shortcuts`` section,
+click the wrench (on the right side).
+
+In the drop-down menu, click ``Delete``.
+
+.. figure:: images/delete_shortcuts.png
+   :alt: Choosing which shortcuts to delete
+
+   Choosing which shortcuts to delete
+
+Every user-defined shortcut (note: global shortcuts are not deletable in
+the webinterface at all) will receive checkbox and ``Delete`` and
+``Cancel`` buttons appear at the bottom of the shortcuts section.
+
+Check all shortcuts which are to be deleted and click ``Delete`` or
+click ``Cancel`` to cancel the deletion.
+
+All deleted shortcuts are marked as deleted afterwards and will not
+appear again in the shortcuts section after reload.
+
+Basic Shortcut
+--------------
+
+The ``Description`` is a verbose definition of the query, e.g. “Search
+for experiments and return a table.”. It will be the text that is
+visible in the shortcuts section.
+
+The ``Query`` is the query that will be executed with the shortcut. It
+adheres to the definition of the CaosDB Query Language (CQL).
+
+The corresponding query of our example is
+``SELECT date, name FROM Experiment``.
+
+Advanced Shortcut
+-----------------
+
+The basic shortcut does not allow for any parameterization. It is just a
+plain string or like a bookmark.
+
+Advanced shortcuts use a special syntax, where text placeholders are
+used to define parameters of the shortcut. The parameters can be set by
+the user at the time of the execution. An example can best illustrate
+what that means:
+
+Suppose you want to search for experiments by their year. The query for
+that would be ``SELECT date, name FROM Experiment WITH date IN 2018``.
+
+Now, the actual year in the query can be made editable by replacing the
+year ``2018`` with ``{year}``.
+
+The ``Description`` now must also contain this placeholder ``{year}``,
+e.g. “Search for experiements conducted in year {year}”. When the
+shortcut is displayed in the shortcuts section below the query input
+field, the placeholder is replaced by a text input field and the user
+can insert a year and execute the shortcut with the year being inserted
+into the query.
+
+Placeholders
+~~~~~~~~~~~~
+
+The placeholders have simple rules. A placeholder always starts and ends
+with curly brackets, like in the example ``{year}``. The text inside the
+brackets (the placeholder’s *id*) may contain any combination of
+alphanumeric signs (0-9,a-z,A-Z). The use of special characters like
+colons, commas or the like is discouraged. They are reserved for future
+extensions of the placeholders. Apart from that, you are free to choose
+any placeholder *id* that seems suitable for you.
+
+Both components of the query shortcut (description and query) must
+contain the same set of placeholders, otherwise the query shortcuts
+might not work as intended. If there is a ``{year}`` in the query, there
+must be a ``{year}`` in the description.
+
+Each placeholder *id* must occur only once in both components – if you
+need to use two years in your shortcut you have to use ``{year1}`` and
+``{year2}`` or any other combinations of placeholder *ids*.
+
+Example for global_query_shortcuts.json
+---------------------------------------
+
+The following example for the file global_query_shortcuts.json would create two global query shortcuts for finding experiments. The second example includes a variable for specifying the year.
+
+.. code-block:: json
+
+    [
+        {
+            "description": "Show a list of all Experiments",
+            "query": "FIND Record Experiment"
+        },
+        {
+            "description": "Show a table of Experiments for year: {year}",
+            "query": "SELECT date, project, identifier FROM Record Experiment with date in {year}"
+        },
+    ]
+
+Data Model for User Query Templates
+-----------------------------------
+
+The current default data model for CaosDB does not include the RecordTypes which are needed for the user query templates. See https://gitlab.indiscale.com/caosdb/src/caosdb-webui/-/issues/104 for details.
+
+The solution is to create the RecordTypes, e.g. using the Python interface, as follows:
+
+.. code-block:: python
+
+    datamodel = caosdb.Container()
+    datamodel.extend([
+        caosdb.Property("Query", datatype=caosdb.TEXT),
+        caosdb.Property("templateDescription", datatype=caosdb.TEXT),
+        caosdb.RecordType(
+            "UserTemplate"
+            ).add_property("Query", importance=caosdb.OBLIGATORY
+            ).add_property("templateDescription", importance=caosdb.OBLIGATORY),
+    ])
+    datamodel.insert()
diff --git a/src/doc/extension/references.rst b/src/doc/extension/references.rst
new file mode 100644
index 0000000000000000000000000000000000000000..63c551612e5e9d807846595b6c5e458bc5096615
--- /dev/null
+++ b/src/doc/extension/references.rst
@@ -0,0 +1,38 @@
+Customizing the display of referenced entities
+=============================================
+
+CaosDB WebUI supports the customized display of referenced entities
+using the :doc:`ext_references <../api/module-resolve_references>`
+module. The ``BUILD_MODULE_EXT_RESOLVE_REFERENCES`` build variable has
+to be set to ``ENABLED`` (see :doc:`/getting_started`) in order to use
+this module.
+
+You may then define your own JavaScript module to define how
+references to specific Records should be resolved. The module has to
+be located at a directory which is known to CaosDB WebUI; we recommend
+``caosdb-webui/src/ext/js``. Set the value of the
+``BUILD_EXT_REFERENCES_CUSTOM_RESOLVER`` build variable to the name of
+this module. The module has to have a ``resolve`` function which takes
+an entity id as its only parameter and returns a ``reference_info``
+object with the resolved custom reference as a ``text`` property. So
+the basic structure of the module should look like
+
+.. code-block:: javascript
+
+   var my_reference_resolver = new function () {
+       // Has to be called ``resolve`` and has to take exactly one
+       // string parameter: the id of the referenced entity.
+       this.resolve = async function (id) {
+	   /*
+	    * find the string that the reference should be resolved to,
+	    * e.g., from the value of the entity's properties.
+	    */
+	   return {"text": new_reference_text}
+       }
+   }
+
+An example is located in
+``caosdb-webui/src/ext/js/person_reference_resolver.js``. It resolves
+any reference to a ``Person`` Record to the value of its ``firstname``
+and ``lastname`` properties separated by a space and is active by
+default.
diff --git a/src/ext/js/person_reference_resover.js b/src/ext/js/person_reference_resover.js
new file mode 100644
index 0000000000000000000000000000000000000000..393557354904787f04472585bca0883d64200d86
--- /dev/null
+++ b/src/ext/js/person_reference_resover.js
@@ -0,0 +1,65 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH (info@indiscale.com)
+ * Copyright (C) 2021 Florian Spreckelsen (f.spreckelsen@indiscale.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+/**
+ * @module person_reference
+ *
+ * Replace the reference to a Person Record by the values of that
+ * Record's firstname and lastname properties.
+ *
+ * TODO: Make name(s) of person RecordType(s) and names of firstname
+ * and lastname properties configurable.
+ */
+var person_reference = new function () {
+
+    var logger = log.getLogger("person_reference");
+
+    const lastname_prop_name = "lastname"
+    const firstname_prop_name = "firstname"
+    const person_rt_name = "Person"
+
+    /**
+     * Return the name of a person as firstname + lastname
+     */
+    this.get_person_str = function (el) {
+  var valpr = getProperties(el);
+  if (valpr == undefined) {
+      return;
+  }
+  return valpr.filter(valprel =>
+      valprel.name.toLowerCase().trim() ==
+    firstname_prop_name.toLowerCase())[0].value +
+      " " +
+      valpr.filter(valprel => valprel.name.toLowerCase().trim() ==
+       lastname_prop_name.toLowerCase())[0].value;
+    }
+
+    this.resolve = async function (id) {
+
+  const entity = (await resolve_references.retrieve(id))[0];
+
+  if (resolve_references.is_child(entity, person_rt_name)) {
+      return {"text": person_reference.get_person_str(entity)};
+  }
+    }
+}
diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js
index bbc1cac0c7eff6bb06c6826c6efb7054827e82c3..e5ff1e8700b8349cddbdda0a0b52ffef47e4e75f 100644
--- a/test/core/js/modules/entity.xsl.js
+++ b/test/core/js/modules/entity.xsl.js
@@ -317,7 +317,7 @@ function applyTemplates(xml, xsl, mode, select = "*") {
     return xslt(xml, modXsl);
 }
 
-function callTemplate(xsl, template, params, wrap_call) {
+function callTemplate(xsl, template, params, wrap_call, root) {
     let entryRuleStart = '<xsl:call-template name="' + template + '">';
     let entryRuleEnd = '</xsl:call-template>';
     var entryRule = entryRuleStart;
@@ -331,5 +331,6 @@ function callTemplate(xsl, template, params, wrap_call) {
     entryRule = '<xsl:template xmlns="http://www.w3.org/1999/xhtml" priority="9" match="/">' +
         entryRule + '</xsl:template>';
     let modXsl = injectTemplate(xsl, entryRule);
-    return xslt(str2xml('<root/>'), modXsl);
+    root = root || '<root/>';
+    return xslt(str2xml(root), modXsl);
 }
diff --git a/test/core/js/modules/ext_bookmarks.js.js b/test/core/js/modules/ext_bookmarks.js.js
index 831df74231e479d5b4550524f6bc0da617c98fb3..8130fe422a5bd124ef1c6767708b036e2e904f53 100644
--- a/test/core/js/modules/ext_bookmarks.js.js
+++ b/test/core/js/modules/ext_bookmarks.js.js
@@ -42,16 +42,18 @@ QUnit.module("ext_bookmarks.js", {
     }
 });
 
-QUnit.test("parse_uri", function(assert) {
+QUnit.test("parse_uri", function (assert) {
     assert.equal(typeof ext_bookmarks.parse_uri(""), "undefined");
     assert.equal(typeof ext_bookmarks.parse_uri("asdf"), "undefined");
     assert.equal(typeof ext_bookmarks.parse_uri("https://localhost:1234/Entity/sada?sadfasd#sdfgdsf"), "undefined");
 
-    assert.propEqual(ext_bookmarks.parse_uri("safgsa/123&456&789?sadfasdf#_bm_1"),
-        {bookmarks: ["123", "456", "789"], collection_id: "1"});
+    assert.propEqual(ext_bookmarks.parse_uri("safgsa/123&456&789?sadfasdf#_bm_1"), {
+        bookmarks: ["123", "456", "789"],
+        collection_id: "1"
+    });
 });
 
-QUnit.test("get_bookmarks, clear_bookmark_storage", function(assert) {
+QUnit.test("get_bookmarks, clear_bookmark_storage", function (assert) {
     assert.propEqual(ext_bookmarks.get_bookmarks(), []);
 
     ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("sdfg")] = "3456"
@@ -67,9 +69,9 @@ QUnit.test("get_export_table", async function (assert) {
     const NEWL = "%0A";
     const context_root = connection.getBasePath() + "Entity/";
     var table = await ext_bookmarks.get_export_table(
-      ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]);
+        ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]);
     assert.equal(table,
-      `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${TAB}Name${TAB}RecordType${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${TAB}${TAB}${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${TAB}${TAB}${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${TAB}${TAB}${NEWL}101112${TAB}abcHead${TAB}${context_root}101112@abcHead${TAB}testpath_101112${TAB}${TAB}${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415${TAB}${TAB}`);
+        `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${TAB}Name${TAB}RecordType${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${TAB}${TAB}${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${TAB}${TAB}${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${TAB}${TAB}${NEWL}101112${TAB}abcHead${TAB}${context_root}101112@abcHead${TAB}testpath_101112${TAB}${TAB}${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415${TAB}${TAB}`);
 
 });
 
@@ -115,7 +117,7 @@ QUnit.test("update_export_link", function (assert) {
 
 QUnit.test("update_collection_link", function (assert) {
     const collection_link = $(
-      `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`);
+        `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`);
     const a = collection_link.find("a")[0];
     $("body").append(collection_link);
 
@@ -148,7 +150,8 @@ QUnit.test("bookmark buttons", function (assert) {
     const non_button = $(`<div data-bla="sadf"/>)`);
     const outside_button = $(`<div data-bmval="id3"/>`);
     const inside_buttons = $("<div/>").append([inactive_button, active_button,
-      broken_button, non_button]);
+        broken_button, non_button
+    ]);
 
     // get_bookmark_buttons
     assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 0);
@@ -174,8 +177,51 @@ QUnit.test("bookmark buttons", function (assert) {
     assert.ok(inactive_button.is(".active"));
 
     ext_bookmarks.clear_bookmark_storage();
-    assert.notOk(inactive_button.is(".active"), "clear_bookmark_storage removes active class");
 
     inside_buttons.remove();
     outside_button.remove();
 });
+
+QUnit.test("select-query transformation", function (assert) {
+    assert.equal(
+        ext_bookmarks.get_select_id_query_string("FIND analysis"),
+        "SELECT ID FROM analysis");
+    assert.equal(
+        ext_bookmarks.get_select_id_query_string(
+            "FIND RECORD analysis WHICH HAS A date > 2012"),
+        "SELECT ID FROM RECORD analysis WHICH HAS A date > 2012");
+    assert.equal(
+        ext_bookmarks.get_select_id_query_string(
+            "SELECT name, date FROM analysis"),
+        "SELECT ID FROM analysis");
+    assert.equal(
+        ext_bookmarks.get_select_id_query_string("COUNT analysis"),
+        "SELECT ID FROM analysis");
+    assert.equal(
+        ext_bookmarks.get_select_id_query_string("fInD analysis"),
+        "SELECT ID FROM analysis");
+});
+
+QUnit.test("select-query extraction", function (assert) {
+    // Use response field copied from demo
+    const response_field = $(`<div class="card caosdb-query-response mb-2">
+    <div class="card-header caosdb-query-response-heading">
+         <div class="row">
+              <div class="col-sm-10 caosdb-overflow-box">
+                   <div class="caosdb-overflow-content">
+                        <span>Query: </span>
+                        <span class = "caosdb-f-query-response-string">SELECT name, id FROM RECORD MusicalAnalysis</span>
+                   </div>
+              </div>
+              <div class="col-sm-2 text-end">
+                   <span>Results: </span>
+                   <span class="caosdb-query-response-results">3</span>
+              </div>
+         </div>
+    </div>
+</div>`);
+    $("body").append(response_field);
+
+    assert.equal(ext_bookmarks.get_query_from_response(),
+        "SELECT ID FROM RECORD MusicalAnalysis");
+});
\ No newline at end of file
diff --git a/test/core/js/modules/ext_cosmetics.js.js b/test/core/js/modules/ext_cosmetics.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5d4df7f10a2859bcd7318680d4f6720aedc6127
--- /dev/null
+++ b/test/core/js/modules/ext_cosmetics.js.js
@@ -0,0 +1,87 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+'use strict';
+
+QUnit.module("ext_cosmetics.js", {
+    before: function (assert) {
+        cosmetics.init();
+        // setup before module
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("linkify - https", function (assert) {
+    assert.ok(cosmetics.linkify, "linkify available");
+    var test_cases = [
+        ["https://link", 1],
+        ["this is other text https://link", 1],
+        ["https://link this is other text", 1],
+        ["this is other text https://link and this as well", 1],
+        ["this is other text https://link", 1],
+        ["this is other text https://link and here comes another link https://link and more text", 2],
+    ];
+    for (let test_case of test_cases) {
+        var text_value = $(`<div class="caosdb-f-property-text-value">${test_case[0]}</div>`);
+        $(document.body).append(text_value);
+        assert.equal($(text_value).find("a[href='https://link']").length, 0, "no link present");
+        cosmetics.linkify();
+        assert.equal($(text_value).find("a[href='https://link']").length, test_case[1], "link is present");
+        text_value.remove();
+    }
+});
+
+QUnit.test("linkify - http", function (assert) {
+    var test_cases = [
+        ["http://link", 1],
+        ["this is other text http://link", 1],
+        ["http://link this is other text", 1],
+        ["this is other text http://link and this as well", 1],
+        ["this is other text http://link", 1],
+        ["this is other text http://link and here comes another link http://link and more text", 2],
+    ];
+    for (let test_case of test_cases) {
+        var text_value = $(`<div class="caosdb-f-property-text-value">${test_case[0]}</div>`);
+        $(document.body).append(text_value);
+        assert.equal($(text_value).find("a[href='http://link']").length, 0, "no link present");
+        cosmetics.linkify();
+        assert.equal($(text_value).find("a[href='http://link']").length, test_case[1], "link is present");
+        text_value.remove();
+    }
+});
+
+QUnit.test("linkify cut-off (40)", function (assert) {
+    var test_case = "here is some text https://this.is.a.link/with/more/than/40/characters/ this is more text";
+    var text_value = $(`<div class="caosdb-f-property-text-value">${test_case}</div>`);
+    $(document.body).append(text_value);
+    assert.equal($(text_value).find("a").length, 0, "no link present");
+    cosmetics.linkify();
+    assert.equal($(text_value).find("a").length, 1, "link is present");
+    assert.equal($(text_value).find("a").text(), "https://this.is.a.link/with/more/th[...] ", "link text has been cut off");
+    text_value.remove();
+});
\ No newline at end of file
diff --git a/test/core/js/modules/ext_qrcode.js.js b/test/core/js/modules/ext_qrcode.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4d505913035d17d14cb7b110e8dc67b0a018a44
--- /dev/null
+++ b/test/core/js/modules/ext_qrcode.js.js
@@ -0,0 +1,101 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+'use strict';
+
+QUnit.module("ext_qrcode.js", {
+    before: function (assert) {
+        // setup before module
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+        $(document.body).append('<div data-entity-id="eid123" data-version-id="vid234" id="ext-qrcode-test-entity" class="caosdb-entity-panel"><div class="caosdb-v-entity-header-buttons-list"></div></div>');
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+        const modal = bootstrap.Modal.getInstance($(".modal")[0]);
+        if (modal) modal.dispose();
+        $("#ext-qrcode-test-entity").remove();
+        $(".modal").remove();
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("init", function (assert) {
+    assert.ok(ext_qrcode.init, "init available");
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button before.");
+    ext_qrcode.init();
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "button has been added.");
+    ext_qrcode.init();
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "still only one button.");
+
+    ext_qrcode.remove_qrcode_button($("#ext-qrcode-test-entity")[0]);
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button after removal.");
+});
+
+QUnit.test("create_qrcode_button", function (assert) {
+    assert.equal(ext_qrcode.create_qrcode_button("entityid", "versionid").tagName, "BUTTON", "create_qrcode_button creates a button");
+});
+
+
+QUnit.test("qrcode_button_click_handler", function (assert) {
+    var done = assert.async();
+    assert.equal($("#qrcode-modal-entityid-versionid").length, 0, "no modal before first click");
+    ext_qrcode.qrcode_button_click_handler("entityid", "versionid")
+    $("#qrcode-modal-entityid-versionid").on("shown.bs.modal", done);
+    assert.equal($("#qrcode-modal-entityid-versionid").length, 1, "first click added the modal");
+});
+
+QUnit.test("update_qrcode", async function (assert) {
+    // create modal
+    const entity_id = "eid456";
+    const entity_version = "vid3564";
+    const modal_id = `qrcode-modal-${entity_id}-${entity_version}`;
+    const modal_element = ext_qrcode.create_qrcode_modal(modal_id, entity_id, entity_version);
+    $(document.body).append(modal_element);
+
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode *").length, 0, "no qrcode.");
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link *").length, 0, "no link.");
+
+    // update adds qrcode
+    ext_qrcode.update_qrcode(modal_element, entity_id, entity_version);
+
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link a")[0].href, connection.getEntityUri([entity_id]), "link points to entity head.");
+    // wait until qrcode is ready
+    await sleep(500);
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 1, "qrcode is there.");
+
+    $("#" + modal_id).find("canvas").remove();
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 0, "removed qrcode canvas for next test.");
+    // select radio button for link to exact version: check both...
+    $("#" + modal_id).find("input[name=entity-qrcode-versioned]").prop("checked", true);
+    // ...then uncheck first
+    $("#" + modal_id).find("input[name=entity-qrcode-versioned]").first().prop("checked", false);
+    $("#" + modal_id).find("form").trigger("change");
+
+    // check: uri has changed
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link a")[0].href, connection.getEntityUri([entity_id]) + "@" + entity_version, "link changed to versioned entity.");
+    // wait until qrcode is ready
+    await sleep(500);
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 1, "qrcode there again.");
+
+});
\ No newline at end of file
diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js
index 43cc1ddd742d6702232b740bbfd96411f41b08f5..54e06d33d5f1c33781efe11802a7fbfc5ba44d89 100644
--- a/test/core/js/modules/ext_references.js.js
+++ b/test/core/js/modules/ext_references.js.js
@@ -104,7 +104,7 @@ QUnit.test("is_child", function(assert){
 });
 
 QUnit.test("get_person_str", function(assert){
-    assert.ok(resolve_references.get_person_str);
+    assert.ok(person_reference.get_person_str);
 });
 
 QUnit.test("update_visible_references_without_summary", async function(assert){
diff --git a/test/core/js/modules/query.xsl.js b/test/core/js/modules/query.xsl.js
index 371b51598918e2fd6bb5d94ca13a94337ccd322e..e644a674b6a8e58fd7c1395d1e337d5416c2bc5e 100644
--- a/test/core/js/modules/query.xsl.js
+++ b/test/core/js/modules/query.xsl.js
@@ -28,19 +28,25 @@ QUnit.module("query.xsl", {
         // load query.xsl
         var done = assert.async();
         var qunit_obj = this;
-        $.ajax({
-            cache: true,
-            dataType: 'xml',
-            url: "xsl/query.xsl",
-        }).done(function(data, textStatus, jdXHR) {
-            insertParam(data, "entitypath", "/entitypath/");
-            qunit_obj.queryXSL = data;
-        }).always(function() {
+        _retrieveQueryXSL().then(function(xsl) {
+            qunit_obj.queryXSL = xsl;
             done();
         });
     }
 });
 
+async function _retrieveQueryXSL() {
+    var queryXsl = await transformation.retrieveXsltScript("query.xsl");
+    var entityXsl = await transformation.retrieveXsltScript("entity.xsl");
+    var commonXsl = await transformation.retrieveXsltScript("common.xsl");
+    var xsl = transformation.mergeXsltScripts(entityXsl, [commonXsl, queryXsl]);
+    insertParam(xsl, "entitypath", "/entitypath/");
+    insertParam(xsl, "filesystempath", "/filesystempath/");
+    insertParam(xsl, "lowercase", "abcdefghijklmnopqrstuvwxyz");
+    insertParam(xsl, "uppercase", "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+    return xsl;
+}
+
 /* TESTS */
 QUnit.test("availability", function(assert) {
     assert.ok(this.queryXSL);
@@ -183,6 +189,35 @@ QUnit.test("template select-table-row ", function(assert){
     assert.equal(next.tagName, "A", "tagName = A");
 });
 
+QUnit.test("template select-table-cell (with version) ", function(assert){
+  let cell = callTemplate(this.queryXSL, "select-table-cell", {"version-id": "vid-2345", "entity-id": "eid-1234", "field-name": "name"}, (x) => `<table><tbody><tr>${x}</tr></tbody></table>`,`<Response><Entity id="eid-1234" name="the-name"><Version id="vid-2345"/></Entity></Response>`);
+    var next = cell.firstElementChild;
+    assert.equal(next.tagName, "TABLE", "tagName = TABLE");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TBODY", "tagName = TBODY");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TR", "tagName = TR");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TD", "tagName = TD");
+    next = next.textContent;
+    assert.equal(next, "the-name", "name = the-name");
+});
+
+
+QUnit.test("template select-table-cell (id only) ", function(assert){
+    let cell = callTemplate(this.queryXSL, "select-table-cell", {"entity-id": "eid-1234", "field-name": "id"}, (x) => `<table><tbody><tr>${x}</tr></tbody></table>`,`<Response><Entity id="eid-1234"/></Response>`);
+    var next = cell.firstElementChild;
+    assert.equal(next.tagName, "TABLE", "tagName = TABLE");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TBODY", "tagName = TBODY");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TR", "tagName = TR");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TD", "tagName = TD");
+    next = next.textContent;
+    assert.equal(next, "eid-1234", "id = eid-1234");
+});
+
 /* MISC FUNCTIONS */
 function getQueryForm(queryXSL) {
     var html = callTemplate(queryXSL, "caosdb-query-panel", {});
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 52bf4ada52d2ce59b59d8615c89ca5796343622b..d2ef27952e41142a62eb70e144571bc9d30c52d2 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -91,28 +91,6 @@ QUnit.test("injectTemplate", async function (assert) {
     assert.equal(xml2str(result_xml), "<newroot xmlns=\"http://www.w3.org/1999/xhtml\">content</newroot>");
 });
 
-QUnit.test("getEntityId", function (assert) {
-    assert.ok(getEntityId, "function available");
-    let okElem = $('<div><div class="caosdb-id">1234</div></div>')[0];
-    let notOkElem = $('<div><div class="caosdb-id">asdf</div></div>')[0];
-    let emptyElem = $('<div></div>')[0];
-
-    assert.throws(() => {
-        getEntityId();
-    }, "no parameter throws");
-    assert.throws(() => {
-        getEntityId(null);
-    }, "null parameter throws");
-    assert.throws(() => {
-        getEntityId(notOkElem);
-    }, "on-integer ID throws");
-    assert.throws(() => {
-        getEntityId(empty);
-    }, "empty elem throws");
-
-    assert.equal("1234", getEntityId(okElem), "ID found");
-});
-
 QUnit.test("asyncXslt", function (assert) {
     let xml_str = '<root/>';
     let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot/></xsl:template></xsl:stylesheet>';
@@ -731,12 +709,6 @@ QUnit.test("removeAllErrorNotifications", function (assert) {
         '<div class="caosdb-preview-waiting-notification">Please wait!</div></div>')[0];
     let emptyElem = $('<div></div>')[0];
 
-    assert.throws(() => {
-        preview.removeAllErrorNotifications();
-    }, "no parameter throws");
-    assert.throws(() => {
-        preview.removeAllErrorNotifications(null);
-    }, "null parameter throws");
 
     assert.equal(okElem.childNodes.length, 3, "before: three children");
     assert.equal(okElem, preview.removeAllErrorNotifications(okElem), "return first parameter");
@@ -753,13 +725,6 @@ QUnit.test("removeAllWaitingNotifications", function (assert) {
         '<div class="caosdb-preview-error-notification">Error!</div></div>')[0];
     let emptyElem = $('<div></div>')[0];
 
-    assert.throws(() => {
-        removeAllWaitingNotifications();
-    }, "no parameter throws");
-    assert.throws(() => {
-        removeAllWaitingNotifications(null);
-    }, "null parameter throws");
-
     assert.equal(okElem.childNodes.length, 3, "before: three children");
     assert.equal(okElem, removeAllWaitingNotifications(okElem), "return first parameter");
     assert.equal(okElem.childNodes.length, 1, "after: one child");
@@ -920,7 +885,7 @@ QUnit.test("createCarouselNav", function (assert) {
         assert.equal($(carousel).find("." + preview.classNamePreviewCarouselNav).length, 1, "carousel has nav");
         assert.equal($(carousel).find(".carousel-inner").length, 1, "carousel has inner");
         for (let i = 0; i < correct_order_id.length; i++) {
-            assert.equal(getEntityId($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order")
+            assert.equal(getEntityID($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order")
         }
 
         assert.ok(carousel.id, "has id");
diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile
index f90db2f9b2b869e12f52de595b7a7677f52374b5..2880d864bc7df86bf7cade4d6c232b387e513bfd 100644
--- a/test/docker/Dockerfile
+++ b/test/docker/Dockerfile
@@ -9,6 +9,8 @@ RUN  apt-get update \
     && apt-get install -y \
       firefox-esr gettext-base python3-pip \
       python3-httpbin git curl x11-apps xvfb unzip \
+      libhdf5-dev \
+      pkgconf \
       nodejs # Don't install `npm` (Debian), it conflicts with the `nodejs` (Node) package \
     && apt-get install -f