diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 79f7cbfeb42794625970722f3d4c52b274969092..b9a09010bb3818f2f551e3bcd1a0b985ce08bcb4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -95,8 +95,7 @@ pages_prepare: &pages_prepare
     refs:
       - /^release-.*$/i
   script:
-    - npm install jsdoc
-    - npm install jsdoc-sphinx
+    - npm install jsdoc jsdoc-sphinx
     - echo "Deploying"
     - make doc
     - rm -r public || true ; cp -r build/doc/html public
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/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md
index 77a95da1cc40c815e4952a1283d345af56e80461..35c6d01c5904289b77fc7f1de9419ef91a1510e9 100644
--- a/.gitlab/merge_request_templates/Default.md
+++ b/.gitlab/merge_request_templates/Default.md
@@ -1,30 +1,35 @@
 # Summary
 
-    Insert a meaningful description for this merge request here.  What is the
-    new/changed behavior? Which bug has been fixed? Are there related Issues?
+*Insert a meaningful description for this merge request here:  What is the new/changed behavior?
+Which bug has been fixed? Are there related issues?*
+
 
 # Focus
 
-    Point the reviewer to the core of the code change. Where should they start
-    reading? What should they focus on (e.g. security, performance,
-    maintainability, user-friendliness, compliance with the specs, finding more
-    corner cases, concrete questions)?
+*Point the reviewer to the core of the code change. Where should they start reading? What should
+they focus on (e.g. security, performance, maintainability, user-friendliness, compliance with the
+specs, finding more corner cases, concrete questions)?*
+
 
 # Test Environment
 
-    How to set up a test environment for manual testing?
+*How to set up a test environment for manual testing?*
+
 
 # Check List for the Author
 
-Please, prepare your MR for a review. Be sure to write a summary and a
-focus and create gitlab comments for the reviewer. They should guide the
-reviewer through the changes, explain your changes and also point out open
-questions. For further good practices have a look at [our review
+Please, prepare your MR for a review. Be sure to write a summary and a focus and create gitlab
+comments for the reviewer. They should guide the reviewer through the changes, explain your changes
+and also point out open questions. For further good practices have a look at [our review
 guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md)
 
 - [ ] All automated tests pass
-- [ ] Reference related Issues
-- [ ] Up-to-date CHANGELOG.md
+- [ ] Reference related issues
+- [ ] Up-to-date CHANGELOG.md (or not necessary)
+- [ ] Up-to-date JSON schema (or not necessary)
+- [ ] Appropriate user and developer documentation (or not necessary)
+  - How do I use the software?  Assume "stupid" users.
+  - How do I develop or debug the software?  Assume novice developers.
 - [ ] Annotations in code (Gitlab comments)
   - Intent of new code
   - Problems with old code
@@ -33,14 +38,14 @@ guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md)
 
 # Check List for the Reviewer
 
-
 - [ ] I understand the intent of this MR
 - [ ] All automated tests pass
-- [ ] Up-to-date CHANGELOG.md
-- [ ] The test environment setup works and the intended behavior is
-  reproducible in the test environment
+- [ ] Up-to-date CHANGELOG.md (or not necessary)
+- [ ] Appropriate user and developer documentation (or not necessary)
+- [ ] The test environment setup works and the intended behavior is reproducible in the test
+  environment
 - [ ] In-code documentation and comments are up-to-date.
-- [ ] Check: Are there spezifications? Are they satisfied?
+- [ ] Check: Are there specifications? Are they satisfied?
 
 For further good practices have a look at [our review guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md).
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1456f1850733a6f9f9d5d286634ee32d4639b504..0f11b523cb90f1ca7ac96e4891d426d78d033d17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,16 +4,14 @@ 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.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased]
+## [Unreleased] - 2023-?
 
 ### Added
 
-### Changed (for changes in existing functionality)
+* `caosdb-v-property-linkified` css class to denote properties that have been
+  linkified already.
 
-* Version bump of caosdb_map module (0.5.0):
-  * Added configurable entityLayers
-  * Changed name of the icon option to icon_options, because that name better
-    distiguished the options from the result icon object.
+### Changed (for changes in existing functionality)
 
 ### Deprecated
 
@@ -21,10 +19,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 
+* [#199](https://gitlab.com/caosdb/caosdb-webui/-/issues/199) - Linkify creates
+  additional links after showing previews of referenced entities
+
 ### Security
 
 ### Documentation
 
+## [0.10.1] - 2023-02-14
+
+### Fixed
+
+* [#194](https://gitlab.com/caosdb/caosdb-webui/-/issues/194) - Properties
+  remain hidden in previews of referenced entities
+
+## [0.10.0] - 2022-12-19
+(Florian Spreckelsen)
+
+### Added
+
+* [#191](https://gitlab.com/caosdb/caosdb-webui/-/issues/191) - "Configure the
+  RecordType which is searched by the simple search."
+  A list of entity roles and names can be specified via the newly added
+  `BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS` build variable. See the docstring
+  of `queryForm.initFreeSearch` for more information.
+* [#188](https://gitlab.com/caosdb/caosdb-webui/-/issues/188) Properties can be
+  hidden/shown only for certain users/roles.
+* [#189](https://gitlab.com/caosdb/caosdb-webui/-/issues/189) The order in which
+  the properties of (Records of) a RecordType are displayed can be configured.
+
+### Changed (for changes in existing functionality)
+
+* Version bump of caosdb_map module (0.5.0):
+  * Added configurable entityLayers
+  * Changed name of the icon option to icon_options, because that name better
+    distiguished the options from the result icon object.
+* New behavior of the "Enter" key in the query input text field: When pressed
+  when the autocompletion drop-down is open, the enter key selects an option
+  from the autocompletion (this is as it was before). But when the "Enter" key
+  is pressed, when the autocompletion drop-down is not open, the query is being
+  submitted.
+
 ## [0.9.0] - 2022-11-02
 (Florian Spreckelsen)
 
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 0000000000000000000000000000000000000000..154773a55f4d9e070e81505287ccaf4669ecc2d9
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,25 @@
+cff-version: 1.2.0
+message: "If you use this software, please cite it as below."
+authors:
+  - family-names: Fitschen
+    given-names: Timm
+    orcid: https://orcid.org/0000-0002-4022-432X
+  - family-names: Schlemmer
+    given-names: Alexander
+    orcid: https://orcid.org/0000-0003-4124-9649
+  - family-names: Hornung
+    given-names: Daniel
+    orcid: https://orcid.org/0000-0002-7846-6375
+  - family-names: tom Wörden
+    given-names: Henrik
+    orcid: https://orcid.org/0000-0002-5549-578X
+  - family-names: Parlitz
+    given-names: Ulrich
+    orcid: https://orcid.org/0000-0003-3058-1435
+  - family-names: Luther
+    given-names: Stefan
+    orcid: https://orcid.org/0000-0001-7214-8125
+title: CaosDB - WebUI
+version: 0.10.1
+doi: 10.3390/data4020083
+date-released: 2023-02-14
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 9c6f7e4c7880c488ec116662c2ed49e2bd85d8a1..0b8358bf7f3738fefd64bd1da3fa91d7e6da9afe 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -1,4 +1,4 @@
-* CaosDB Server 0.8.0
+* CaosDB Server 0.9.0
 * Make 4.2.0
 
 # Java Script Libraries (included in this repository)
diff --git a/README.md b/README.md
index dca645c4244573ae31c7e0072208b53a47cec1d2..d6c53f492b2f6080d423aaf6fbee25e5504ca794 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ when creating the merge request. This allows our team to work with you on your r
 - If you have a suggestion for the [documentation](https://docs.indiscale.com/caosdb-webui/),
 the preferred way is also a merge request as describe above (the documentation resides in `src/doc`).
 However, you can also create an issue for it.
-- You can also contact us at **info (AT) caosdb.de**  and join the
+- You can also contact us at **info (AT) caosdb.org**  and join the
   CaosDB community on
   [#caosdb:matrix.org](https://matrix.to/#/!unwwlTfOznjEnMMXxf:matrix.org).
 
diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md
index e9a9bb8154c22648d602ceecc78ee41ac666e310..00f044e142a1d89efd764076e3b6c6c079133fc7 100644
--- a/RELEASE_GUIDELINES.md
+++ b/RELEASE_GUIDELINES.md
@@ -17,8 +17,10 @@ guidelines of the CaosDB Project
 
 2. Check all general prerequisites.
 
-3. Update `src/doc/conf.py` version and check that the correct caosdb-server
-   version is listed in `DEPENDENCIES.md`.
+3. Update the version:
+   - Update `src/doc/conf.py` version and check that the correct caosdb-server
+     version is listed in `DEPENDENCIES.md`.
+   - `CITATION.cff` (update version and date)
 
 4. Merge the release branch into the main branch.
 
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index cd57b632e2ea2b9ab005c7185a309a9594f123ff..9b8f8095befc01f3f9fba610ee61efbaa8c24fc5 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -62,6 +62,7 @@ BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED
 BUILD_EXT_REFERENCES_CUSTOM_RESOLVER=caosdb_default_person_reference
 
 BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT=DISABLED
+BUILD_MODULE_EXT_PROPERTY_DISPLAY=DISABLED
 
 ##############################################################################
 # Navbar properties
@@ -75,6 +76,27 @@ BUILD_NAVBAR_BRAND_NAME=CaosDB
 BUILD_TITLE_BRAND_NAME=CaosDB
 BUILD_FAVICON=pics/caosdb_logo_42.png
 
+##############################################################################
+# queryForm properties
+##############################################################################
+
+# Initialize the free search to generate search queries which search only
+# within one of several options of roles or entity names.
+#
+# E.g. when `options` is "Person, Experiment, Sample" the user can select
+# one of these options before submitting the query. When the user types in
+# something that doesn't looks like a CQL-query, a query is generated
+# instead which goes like: FIND Persion WHICH HAS A PROPERTY LIKE
+# "*something*".
+#
+# Note: This feature is disabled by default. Enable it by specifying the
+# build variable BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS as a
+# comma-separated list of **properly quoted** expressions, e.g.
+# "FILE 'numpy array', 'Plant Experiemnt', 'quotes_not_necessary'".
+# Otherwise, the server will throw a syntax error.
+BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS=
+
+
 ##############################################################################
 # Footer properties
 ##############################################################################
@@ -121,6 +143,7 @@ BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX="Tools"
 # MODULE_DEPENDENCIES array.
 ##############################################################################
 JS_DIST_BUNDLE=TRUE
+
 ##############################################################################
 # TRUE means that all javascript sources which are no mentioned in the
 # MODULE_DEPENDENCIES array will be added in no particular order into the
@@ -128,6 +151,7 @@ JS_DIST_BUNDLE=TRUE
 # appear in the dit file) you need to add them to the MODULE_DEPENDENCIES.
 ##############################################################################
 AUTO_DISCOVER_MODULES=TRUE
+
 ##############################################################################
 # Module dependencies
 # Override or extend to specify the order of js files in the resulting
@@ -146,6 +170,7 @@ MODULE_DEPENDENCIES=(
     webcaosdb.js
     pako.js
     utif.js
+    ext_version_history.js
     caosdb.js
     form_elements.js
     ext_autocomplete.js
@@ -179,4 +204,5 @@ MODULE_DEPENDENCIES=(
     ckeditor.js
     ext_editmode_wysiwyg_text.js
     reference_resolver/caosdb_default_person.js
+    ext_prop_display.js
 )
diff --git a/conf/core/json/ext_prop_display.json b/conf/core/json/ext_prop_display.json
new file mode 100644
index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93
--- /dev/null
+++ b/conf/core/json/ext_prop_display.json
@@ -0,0 +1 @@
+{}
diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css
index 1c8f50be049d308efcac25e41d705b08d96d21e6..248f3444962a839193016a42dc4e9f730ba7d448 100644
--- a/src/core/css/webcaosdb.css
+++ b/src/core/css/webcaosdb.css
@@ -381,6 +381,14 @@ h5 {
     margin-right: 0px;
 }
 
+body[data-hidden-properties="true"] .caosdb-v-hidden-property {
+    display: None;
+}
+
+body[data-hidden-properties="true"] .caosdb-v-entity-being-edited .caosdb-v-hidden-property {
+    display: unset;
+}
+
 .caosdb-v-edit-drag {
     padding: 5px;
 }
diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js
index 63b28b08a6c07d4c5b00795f5e9f87d1965e4031..ba478dcd90cc9f6899e678f543d1c928129e1df7 100644
--- a/src/core/js/edit_mode.js
+++ b/src/core/js/edit_mode.js
@@ -1359,6 +1359,7 @@ var edit_mode = new function () {
         doc.firstElementChild.appendChild(newrecord.firstElementChild);
         // TODO I dunno whats wrong here: xml -> str -> xml ???
         var x = await transformation.transformEntities(str2xml(xml2str(doc)));
+        $(x[0]).addClass("caosdb-v-entity-being-edited");
         return x[0];
     }
 
@@ -1618,6 +1619,8 @@ var edit_mode = new function () {
             app.entity = $(entity).clone(true)[0];
             // remove preview stuff
             $(app.entity).find(".caosdb-preview-container").remove();
+            // add class for styling the entity that's being edited
+            $(app.entity).addClass("caosdb-v-entity-being-edited");
             edit_mode.smooth_replace(app.old, app.entity);
 
             edit_mode.add_save_button(app.entity, () => app.update(app.entity));
@@ -1897,7 +1900,9 @@ var edit_mode = new function () {
 
     this.create_new_entity = async function (role) {
         var empty_entity = str2xml('<Response><' + role + '/></Response>');
-        return (await transformation.transformEntities(empty_entity))[0];
+        var ent_element = await transformation.transformEntities(empty_entity);
+        $(ent_element).addClass("caosdb-v-entity-being-edited");
+        return ent_element[0];
     }
 
     this.remove_cancel_button = function (entity) {
@@ -2097,4 +2102,4 @@ var edit_mode = new function () {
  */
 $(document).ready(function () {
     edit_mode.init();
-});
\ No newline at end of file
+});
diff --git a/src/core/js/ext_autocomplete.js b/src/core/js/ext_autocomplete.js
index 932a9466a83f17594894ad389f6535bcd6caffa2..02ae1199494ae3634afa16ad417b886f19068ee0 100644
--- a/src/core/js/ext_autocomplete.js
+++ b/src/core/js/ext_autocomplete.js
@@ -179,6 +179,19 @@ var ext_autocomplete = new function () {
      */
     this.switch_on_completion = function () {
         var field = $("#caosdb-query-textarea");
+
+        // submit on "enter" when the drop-down menu is not visible.
+        field.on("keydown", (e) => {
+            if(e.originalEvent.keyCode == 13) { // Enter
+                if($(e.target).siblings(".bootstrap-autocomplete.show").length == 0) {
+                    $(".caosdb-search-btn").click();
+                } else {
+                    // don't submit - the user is just selecting something
+                    e.originalEvent.preventDefault();
+                }
+            }
+        });
+
         field.attr("autocomplete", "off");
         field.toggleClass("basicAutoComplete", true);
         field.autoComplete({
diff --git a/src/core/js/ext_cosmetics.js b/src/core/js/ext_cosmetics.js
index 77556437394df6a6763661ce5c0d5001f68ce61a..da05f294f2022a469e74ec05d64211fe1f8b26b7 100644
--- a/src/core/js/ext_cosmetics.js
+++ b/src/core/js/ext_cosmetics.js
@@ -1,8 +1,9 @@
 /*
  * This file is a part of the CaosDB Project.
  *
- * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021-2023 IndiScale GmbH <info@indiscale.com>
  * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2023 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
@@ -37,7 +38,7 @@ var cosmetics = new function () {
 
     var _linkify = function () {
         $('.caosdb-f-property-text-value').each(function (index) {
-            if (/https?:\/\//.test(this.innerText)) {
+            if (!($(this).hasClass("caosdb-v-property-linkified")) && (/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) {
@@ -47,6 +48,9 @@ var cosmetics = new function () {
                     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>`;
                 });
 
+                // add class to highlight that this has been linkified already
+                // (see https://gitlab.com/caosdb/caosdb-webui/-/issues/199).
+                $(this).addClass("caosdb-v-property-linkified")
                 $(this).hide();
                 $(this).after(result);
             }
@@ -80,4 +84,4 @@ var cosmetics = new function () {
 
 $(document).ready(function () {
     caosdb_modules.register(cosmetics);
-});
\ No newline at end of file
+});
diff --git a/src/core/js/ext_prop_display.js b/src/core/js/ext_prop_display.js
new file mode 100644
index 0000000000000000000000000000000000000000..3a8c616f503caadd4c0df04800bc8c15f0c068b2
--- /dev/null
+++ b/src/core/js/ext_prop_display.js
@@ -0,0 +1,248 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022-2023 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2022-2023 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/>.
+ *
+ */
+'use strict';
+
+/**
+ * Hide or show properties for specific roles and users
+ * see src/doc/extension/display_of_properties.rst for documentation
+ *
+ * @requires jQuery (library)
+ * @requires log (singleton from loglevel library)
+ * @requires load_config (function from webcaosdb.js)
+ */
+var prop_display = new function ($, edit_mode, getEntityName, getEntityRole, getPropertyElements, getPropertyName, getUserName, getUserRoles, logger, load_config, preview, query) {
+
+    /**
+     * Return the property-display config file; `ext_prop_display.json` by
+     * default.
+     *
+     * @param {string} resource - file name of the config file
+     */
+    this.load_config = async function (resource) {
+
+        var conf = {};
+        try {
+            resource = resource || "ext_prop_display.json";
+            conf = await load_config(resource);
+        } catch (err) {
+            logger.error(err);
+        }
+
+        return conf;
+    }
+
+    this.getEntitiesInView = function () {
+        // Use all entities, both in entity panel and in preview.
+        return $(".caosdb-entity-panel,.caosdb-entity-preview");
+    }
+
+    this.displayProperties = function (entities, conf, allTypes, userName, userRoles) {
+
+        for (let ent of entities) {
+            let parents = getParents(ent).map(par => par.name);
+            let properties = getPropertyElements(ent);
+            // either the entity has matching parents OR it is the actual
+            // RecordType for which a rule is written.
+            if (parents.some(par => allTypes.allTypesOrChildren.includes(par)) ||
+                (getEntityRole(ent) == "RecordType" && allTypes.allTypesOrChildren.includes(getEntityName(ent)))) {
+                // we know that there is at least one rule for this type (it is
+                // in `allTypes.allTypesOrChildren`), but we don't know which tp
+                // apply yet.
+                for (let typeName of Object.keys(conf)) {
+                    let typeConf = conf[typeName];
+                    let allNames = allTypes.typesWithChildren[typeName];
+                    // only change the display something if there is a match in
+                    // at least one parent type
+                    if (parents.some(par => allNames.includes(par)) ||
+                        (getEntityRole(ent) == "RecordType" && allNames.includes(getEntityName(ent)))) {
+                        // first sort the properties
+                        this._sortProperties(ent, properties, typeConf);
+                        properties.forEach((prop, index) => {
+                            if (this._hide_property(getPropertyName(prop), userName, userRoles, typeConf)) {
+                                // Should be hidden by default but better safe than sorry
+                                $(prop).addClass("caosdb-v-hidden-property").removeClass("caosdb-v-show-property");
+                            } else {
+                                // show this property
+                                $(prop).addClass("caosdb-v-show-property").removeClass("caosdb-v-hidden-property");
+                            }
+                        });
+                    }
+                }
+            } else {
+                // no rules for this RecordType, so show all properties
+                properties.forEach((prop, index) => $(prop).addClass("caosdb-v-show-property").removeClass("caosdb-v-hidden-property"));
+            }
+        }
+    }
+
+    this._sortProperties = function (entity, properties, conf) {
+        if (conf.order == undefined || conf.order.length == 0) {
+            return;
+        }
+        properties.sort(function (a, b) {
+            let confIndexA = conf.order.indexOf(getPropertyName(a));
+            let confIndexB = conf.order.indexOf(getPropertyName(b));
+            if (confIndexA < 0 && confIndexB < 0) {
+                // both are not part of order list
+                return 0;
+            }
+            if (confIndexA < 0) {
+                // only b is part of order list , so it is placed before a
+                return 1;
+            }
+            if (confIndexB < 0) {
+                // only a is part of order list, so it is placed before b
+                return -1;
+            }
+            // From here, we can assume that both are in the order list:
+            return confIndexA - confIndexB;
+        });
+        $(entity).find(".caosdb-properties").append(properties);
+    }
+
+    this._hide_property = function (propname, userName, userRoles, conf) {
+
+        // is this property only shown for certain users/groups?
+        if ((conf.show != undefined) && conf.show.length > 0) {
+            for (let def of conf.show) {
+                if (propname.toLowerCase() == def.name.toLowerCase()) {
+                    if (!(def.users.includes(userName)) && !(userRoles.some(role => def.roles.includes(role)))) {
+                        return true
+                    }
+                }
+            }
+        }
+
+        // is this property hidden for certain users/groups?
+        if ((conf.hide != undefined) && conf.hide.length > 0) {
+            for (let def of conf.hide) {
+                if (propname.toLowerCase() == def.name.toLowerCase()) {
+                    if (def.users.includes(userName) || userRoles.some(role => def.roles.includes(role))) {
+                        return true
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    this._getRecordTypes = async function (conf) {
+
+        const parentTypes = Object.keys(conf);
+
+        var typesWithChildren = {};
+        var allTypesOrChildren = [];
+
+        for (let parentName of parentTypes) {
+            const children = await query(`FIND RECORDTYPE "${parentName}"`);
+            const names = children.map(ent => getEntityName(ent));
+            typesWithChildren[parentName] = names;
+            allTypesOrChildren = allTypesOrChildren.concat(names);
+        }
+
+        return {
+            "typesWithChildren": typesWithChildren,
+            "allTypesOrChildren": allTypesOrChildren
+        };
+    }
+
+    this.unhideAllProperties = function () {
+        // Just show all initially hidden properties
+        $(".caosdb-v-hidden-property").removeClass("caosdb-v-hidden-property");
+    }
+
+    this._unhideAllPropertiesWrapper = function (original) {
+        // construct a function that wirst does the original work, then unhides
+        // all properties, then returns the original return values.
+        const result = function (entity) {
+            var original_return = undefined;
+            if (typeof original === "function") {
+                original_return = original(entity);
+            }
+            prop_display.unhideAllProperties();
+            return original_return;
+        }
+
+        return result;
+    }
+
+    this._displayPropertiesWrapper = function (original, conf, allTypes) {
+        // Same as above, but for when there actually may be something to hide
+        // (i.e., build variable is set and config is non-empty).
+        const result = function (entitiy) {
+            var original_return = undefined;
+            if (typeof original === "function") {
+                original_return = original(entitiy);
+            }
+            var entities = prop_display.getEntitiesInView();
+            const userName = getUserName();
+            const userRoles = getUserRoles();
+            prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+            return original_return;
+        }
+
+        return result;
+    }
+
+    this.init = async function () {
+        const conf = await this.load_config();
+        if (Object.keys(conf).length > 0) {
+            const allTypes = await this._getRecordTypes(conf);
+            var entities = this.getEntitiesInView();
+            const userName = getUserName();
+            const userRoles = getUserRoles();
+            this.displayProperties(entities, conf, allTypes, userName, userRoles);
+            // If we are in the edit mode, (un)hide properties after ending
+            // the editing of an entity
+            document.body.addEventListener(edit_mode.start_edit.type, (e) => {
+                edit_mode.app.onAfterShowResults = this._displayPropertiesWrapper(edit_mode.app.onAfterShowResults, conf, allTypes);
+            }, true);
+            // After showing a preview for the first time, its entity card is
+            // added to the dom tree, so the properties have to be (un)hidden
+            // afterwards.
+            document.body.addEventListener(preview.previewReadyEvent.type, (e) => {
+                let newEntities = $(".caosdb-entity-preview");
+                this.displayProperties(newEntities, conf, allTypes, userName, userRoles);
+            }, true);
+
+        } else {
+            // There are no properties to be hidden, so make this clear in HTML body
+            $("body").attr("data-hidden-properties", "false");
+            this.unhideAllProperties();
+            document.body.addEventListener(edit_mode.start_edit.type, (e) => {
+                // also unhide properties when leaving the edit mode
+                // TODO(fspreck): We're lacking a proper state/event here in the
+                // edit mode, so do this on "init", since this is the state to which
+                // the state machine returns after either successfully saving an
+                // entity or canceling the edit.
+                edit_mode.app.onAfterShowResults = this._unhideAllPropertiesWrapper(edit_mode.app.onAfterShowResults);
+            }, true);
+            document.body.addEventListener(preview.previewReadyEvent.type, (e) => this.unhideAllProperties(), true);
+        }
+    }
+}($, edit_mode, getEntityName, getEntityRole, getPropertyElements, getPropertyName, getUserName, getUserRoles, log.getLogger("ext_prop_display"), load_config, preview, query);
+
+$(document).ready(() => {
+    if ("${BUILD_MODULE_EXT_PROPERTY_DISPLAY}" == "ENABLED") {
+        caosdb_modules.register(prop_display);
+    }
+});
diff --git a/src/core/js/ext_version_history.js b/src/core/js/ext_version_history.js
new file mode 100644
index 0000000000000000000000000000000000000000..ea65481589c2f95405833f7b4a929f450ea6da96
--- /dev/null
+++ b/src/core/js/ext_version_history.js
@@ -0,0 +1,238 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2019-2022 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2019-2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 Florian Spreckelsen <f.spreckelsen@indiscale.com>
+ * Copyright (C) 2022 Daniel Hornung <d.hornung@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
+ */
+
+
+/**
+ * This module provides the functionality to load the full version history (for
+ * privileged users) and export it to tsv.
+ *
+ * @module version_history
+ */
+var version_history = new function () {
+
+    const logger = log.getLogger("version_history");
+    this.logger = logger;
+
+    this._has_version_fragment = function () {
+        const fragment = window.location.hash.substr(1);
+        return fragment === 'version_history';
+    }
+
+    this._get = connection.get;
+    /**
+     * Retrieve the version history of an entity and return a table with the
+     * history.
+     *
+     * @function retrieve_history
+     * @param {string} entity - the entity id with or without version id.
+     * @return {HTMLElement} A table with the version history.
+     */
+    this.retrieve_history = async function (entity) {
+        const xml = this._get(transaction
+            .generateEntitiesUri([entity]) + "?H");
+        const html = (await transformation.transformEntities(xml))[0];
+        const history_table = $(html).find(".caosdb-f-entity-version-history");
+        return history_table[0];
+    }
+
+    /**
+     * Initalize the buttons for loading the version history.
+     *
+     * The buttons are visible when the entity has only the normal version info
+     * attached and the current user has the permissions to retrieve the
+     * version history.
+     *
+     * The buttons trigger the retrieval of the version history and append the
+     * version history to the version info modal.
+     *
+     * @function init_load_history_buttons
+     */
+    this.init_load_history_buttons = function () {
+        for (let entity of $(".caosdb-entity-panel")) {
+            const is_permitted = hasEntityPermission(entity, "RETRIEVE:HISTORY");
+            if (!is_permitted) {
+                continue;
+            }
+            const entity_id_version = getEntityIdVersion(entity);
+            const version_info = $(entity)
+                .find(".caosdb-f-entity-version-info");
+            const button = $(version_info)
+                .find(".caosdb-f-entity-version-load-history-btn");
+            button.show();
+            button
+                .click(async () => {
+                    button.prop("disabled", true);
+                    const wait = createWaitingNotification("Retrieving full history. Please wait.");
+                    const sparse = $(version_info)
+                        .find(".caosdb-f-entity-version-history");
+                    sparse.find(".modal-body *").replaceWith(wait);
+
+                    const history_table = await version_history
+                        .retrieve_history(entity_id_version);
+                    sparse.replaceWith(history_table);
+                    version_history.init_export_history_buttons(entity);
+                    version_history.init_restore_version_buttons(entity);
+                });
+        }
+    }
+
+    /**
+     * Transform the HTML table with the version history to tsv.
+     *
+     * @function get_history_tsv
+     * @param {HTMLElement} history_table - the HTML representation of the
+     *     version history.
+     * @return {string} the version history as downloadable tsv string,
+     *     suitable for the href attribute of a link or window.location.
+     */
+    this.get_history_tsv = function (history_table) {
+        const rows = [];
+        for (let row of $(history_table).find("tr")) {
+            const cells = $(row).find(".export-data").toArray().map(x => x.textContent);
+            rows.push(cells);
+        }
+        return caosdb_utils.create_tsv_table(rows);
+    }
+
+    /**
+     * Initialize the export buttons of `entity`.
+     *
+     * The buttons are only visible when the version history is visible and
+     * trigger a download of a tsv file which contains the version history.
+     *
+     * The buttons trigger the download of a tsv file with the version history.
+     *
+     * @function init_export_history_buttons
+     * @param {HTMLElement} [entity] - if undefined, the export buttons of all
+     *     page entities are initialized.
+     */
+    this.init_export_history_buttons = function (entity) {
+        entity = entity || $(".caosdb-entity-panel");
+        for (let version_info of $(entity)
+                .find(".caosdb-f-entity-version-info")) {
+            $(version_info).find(".caosdb-f-entity-version-export-history-btn")
+                .click(() => {
+                    const html_table = $(version_info).find("table")[0];
+                    const history_tsv = this.get_history_tsv(html_table);
+                    version_history._download_tsv(history_tsv);
+                });
+        }
+    }
+
+    /**
+     * Initialize the restore old version buttons of `entity`.
+     *
+     * The buttons are only visible when the user is allowed to update the
+     * entity.
+     *
+     * The causes a retrieve of the specified version of the entity and then an
+     * update that restores that version.
+     *
+     * @function init_restore_version_buttons
+     * @param {HTMLElement} [entity] - if undefined, the export buttons of all
+     *     page entities are initialized.
+     */
+    this.init_restore_version_buttons = function (entity) {
+        var entities = [entity] || $(".caosdb-entity-panel");
+
+        for (let _entity of entities) {
+            // initialize buttons only if the user is allowed to update the entity
+            if (hasEntityPermission(_entity, "UPDATE:*") || hasEntityPermission(_entity, "UPDATE:DESCRIPTION")) {
+                for (let version_info of
+                        $(_entity).find(".caosdb-f-entity-version-info")) {
+                    // find the restore button
+                    $(version_info).find(".caosdb-f-entity-version-restore-btn")
+                        .toggleClass("d-none", false) // show button
+                        .click(async (eve) => {
+                            // the version id is stored in the restore button's
+                            // data-version-id attribute
+                            const versionid = eve.delegateTarget.getAttribute("data-version-id")
+                            const reload = () => {
+                                window.location.reload();
+                            }
+                            const _alert = form_elements.make_alert({
+                                title: "Warning",
+                                message: "You are going to restore this version of the entity.",
+                                proceed_callback: async () => {
+                                    try {
+                                        await restore_old_version(versionid);
+                                        $(_alert).remove();
+                                        // reload after sucessful update
+                                        $(version_info).find(".modal-body").prepend(
+                                            $(`<div class="alert alert-success" role="alert">Restore successful! <p>You are being forwarded to the latest version of this entity or you can click <a href="#" onclick="window.location.reload()">here</a>.</p></div>`));
+                                        setTimeout(reload, 5000);
+                                    } catch (e) {
+                                        logger.error(e);
+                                        // print errors in an alert div
+                                        $(version_info).find(".modal-body").prepend(
+                                            $(`<div class="alert alert-danger alert-dismissible " role="alert"> <button class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> Restore failed! <p>${e.message}</p></div>`));
+
+                                    }
+                                },
+                                cancel_callback: () => {
+                                    // do nothing
+                                    $(_alert).remove();
+                                    $(version_info).find("table").show();
+                                },
+                                proceed_text: "Yes, restore!",
+                                remember_my_decision_id: "restore_entity",
+                            });
+
+                            $(version_info).find("table").after(_alert).hide();
+                            $(_alert).addClass("text-end");
+                        });
+                }
+            }
+        }
+    }
+
+    this._download_tsv = function (tsv_link) {
+        window.location.href = tsv_link;
+    }
+
+
+    /**
+     * @function init
+     */
+    this.init = function () {
+        this.init_load_history_buttons();
+        this.init_export_history_buttons();
+        this.init_restore_version_buttons();
+
+        // check for the version_history fragment and open the modal if present.
+        if (this._has_version_fragment()) {
+            const first_entity = $(".caosdb-entity-panel")[0];
+            if (first_entity && hasEntityPermission(first_entity, "RETRIEVE:HISTORY")) {
+                logger.debug("Showing full version modal for first entity");
+                const version_button = $(first_entity).find(".caosdb-f-entity-version-button");
+                version_button.click();
+                const full_version_history_button = $(first_entity).find(".caosdb-f-entity-version-load-history-btn");
+                full_version_history_button.click();
+            }
+        }
+    }
+}
+
+caosdb_modules.register(version_history);
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index 03e372919377e537017a0f2917c0d66db9ed580e..bfe0eb35c8e72e6493ad273ae0ed5a727019c628 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -26,6 +26,13 @@
  */
 'use strict';
 
+/**
+ * Core functionality of the CaosDB web interface.
+ *
+ * @module webcaosdb
+ * @global
+ */
+
 window.addEventListener('error', (e) => globalError(e.error));
 
 var globalError = function (error) {
@@ -61,6 +68,9 @@ var globalClassNames = new function () {
 
 /**
  * navbar module contains convenience functions for the navbar.
+ *
+ * @module navbar
+ * @global
  */
 this.navbar = new function () {
 
@@ -299,6 +309,10 @@ this.navbar = new function () {
 }
 
 
+/**
+ * @module caosdb_utils
+ * @global
+ */
 this.caosdb_utils = new function () {
     this.assert_string = function (obj, name, optional = false) {
         if (typeof obj === "undefined" && optional) {
@@ -370,6 +384,9 @@ this.caosdb_utils = new function () {
 
 /**
  * connection module contains all ajax calls.
+ *
+ * @module connection
+ * @global
  */
 this.connection = new function () {
     const logger = log.getLogger("connection");
@@ -544,6 +561,9 @@ this.connection = new function () {
 /**
  * transformation module contains all code for tranforming xml into html via
  * xslt.
+ *
+ * @module transformation
+ * @global
  */
 this.transformation = new function () {
     /**
@@ -656,6 +676,9 @@ this.transformation = new function () {
 /**
  * transaction module contains all code for insertion, update and deletion of 
  * entities. Currently, only updates are implemented.
+ *
+ * @module transaction
+ * @global
  */
 this.transaction = new function () {
     this.classNameUpdateForm = "caosdb-update-entity-form";
@@ -995,205 +1018,9 @@ this.transaction = new function () {
 }
 
 /**
- * This module provides the functionality to load the full version history (for
- * privileged users) and export it to tsv.
+ * @module paging
+ * @global
  */
-var version_history = new function () {
-
-    const logger = log.getLogger("version_history");
-    this.logger = logger;
-
-    this._has_version_fragment = function () {
-        const fragment = window.location.hash.substr(1);
-        return fragment === 'version_history';
-    }
-
-    this._get = connection.get;
-    /**
-     * Retrieve the version history of an entity and return a table with the
-     * history.
-     *
-     * @param {string} entity - the entity id with or without version id.
-     * @return {HTMLElement} A table with the version history.
-     */
-    this.retrieve_history = async function (entity) {
-        const xml = this._get(transaction
-            .generateEntitiesUri([entity]) + "?H");
-        const html = (await transformation.transformEntities(xml))[0];
-        const history_table = $(html).find(".caosdb-f-entity-version-history");
-        return history_table[0];
-    }
-
-    /**
-     * Initalize the buttons for loading the version history.
-     *
-     * The buttons are visible when the entity has only the normal version info
-     * attached and the current user has the permissions to retrieve the
-     * version history.
-     *
-     * The buttons trigger the retrieval of the version history and append the
-     * version history to the version info modal.
-     */
-    this.init_load_history_buttons = function () {
-        for (let entity of $(".caosdb-entity-panel")) {
-            const is_permitted = hasEntityPermission(entity, "RETRIEVE:HISTORY");
-            if (!is_permitted) {
-                continue;
-            }
-            const entity_id_version = getEntityIdVersion(entity);
-            const version_info = $(entity)
-                .find(".caosdb-f-entity-version-info");
-            const button = $(version_info)
-                .find(".caosdb-f-entity-version-load-history-btn");
-            button.show();
-            button
-                .click(async () => {
-                    button.prop("disabled", true);
-                    const wait = createWaitingNotification("Retrieving full history. Please wait.");
-                    const sparse = $(version_info)
-                        .find(".caosdb-f-entity-version-history");
-                    sparse.find(".modal-body *").replaceWith(wait);
-
-                    const history_table = await version_history
-                        .retrieve_history(entity_id_version);
-                    sparse.replaceWith(history_table);
-                    version_history.init_export_history_buttons(entity);
-                    version_history.init_restore_version_buttons(entity);
-                });
-        }
-    }
-
-    /**
-     * Transform the HTML table with the version history to tsv.
-     *
-     * @param {HTMLElement} history_table - the HTML representation of the
-     *     version history.
-     * @return {string} the version history as downloadable tsv string,
-     *     suitable for the href attribute of a link or window.location.
-     */
-    this.get_history_tsv = function (history_table) {
-        const rows = [];
-        for (let row of $(history_table).find("tr")) {
-            const cells = $(row).find(".export-data").toArray().map(x => x.textContent);
-            rows.push(cells);
-        }
-        return caosdb_utils.create_tsv_table(rows);
-    }
-
-    /**
-     * Initialize the export buttons of `entity`.
-     *
-     * The buttons are only visible when the version history is visible and
-     * trigger a download of a tsv file which contains the version history.
-     *
-     * The buttons trigger the download of a tsv file with the version history.
-     *
-     * @param {HTMLElement} [entity] - if undefined, the export buttons of all
-     *     page entities are initialized.
-     */
-    this.init_export_history_buttons = function (entity) {
-        entity = entity || $(".caosdb-entity-panel");
-        for (let version_info of $(entity)
-                .find(".caosdb-f-entity-version-info")) {
-            $(version_info).find(".caosdb-f-entity-version-export-history-btn")
-                .click(() => {
-                    const html_table = $(version_info).find("table")[0];
-                    const history_tsv = this.get_history_tsv(html_table);
-                    version_history._download_tsv(history_tsv);
-                });
-        }
-    }
-
-    /**
-     * Initialize the restore old version buttons of `entity`.
-     *
-     * The buttons are only visible when the user is allowed to update the
-     * entity.
-     *
-     * The causes a retrieve of the specified version of the entity and then an
-     * update that restores that version.
-     *
-     * @param {HTMLElement} [entity] - if undefined, the export buttons of all
-     *     page entities are initialized.
-     */
-    this.init_restore_version_buttons = function (entity) {
-        var entities = [entity] || $(".caosdb-entity-panel");
-
-        for (let _entity of entities) {
-            // initialize buttons only if the user is allowed to update the entity
-            if (hasEntityPermission(_entity, "UPDATE:*") || hasEntityPermission(_entity, "UPDATE:DESCRIPTION")) {
-                for (let version_info of
-                        $(_entity).find(".caosdb-f-entity-version-info")) {
-                    // find the restore button
-                    $(version_info).find(".caosdb-f-entity-version-restore-btn")
-                        .toggleClass("d-none", false) // show button
-                        .click(async (eve) => {
-                            // the version id is stored in the restore button's
-                            // data-version-id attribute
-                            const versionid = eve.delegateTarget.getAttribute("data-version-id")
-                            const reload = () => {
-                                window.location.reload();
-                            }
-                            const _alert = form_elements.make_alert({
-                                title: "Warning",
-                                message: "You are going to restore this version of the entity.",
-                                proceed_callback: async () => {
-                                    try {
-                                        await restore_old_version(versionid);
-                                        $(_alert).remove();
-                                        // reload after sucessful update
-                                        $(version_info).find(".modal-body").prepend(
-                                            $(`<div class="alert alert-success" role="alert">Restore successful! <p>You are being forwarded to the latest version of this entity or you can click <a href="#" onclick="window.location.reload()">here</a>.</p></div>`));
-                                        setTimeout(reload, 5000);
-                                    } catch (e) {
-                                        logger.error(e);
-                                        // print errors in an alert div
-                                        $(version_info).find(".modal-body").prepend(
-                                            $(`<div class="alert alert-danger alert-dismissible " role="alert"> <button class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> Restore failed! <p>${e.message}</p></div>`));
-
-                                    }
-                                },
-                                cancel_callback: () => {
-                                    // do nothing
-                                    $(_alert).remove();
-                                    $(version_info).find("table").show();
-                                },
-                                proceed_text: "Yes, restore!",
-                                remember_my_decision_id: "restore_entity",
-                            });
-
-                            $(version_info).find("table").after(_alert).hide();
-                            $(_alert).addClass("text-end");
-                        });
-                }
-            }
-        }
-    }
-
-    this._download_tsv = function (tsv_link) {
-        window.location.href = tsv_link;
-    }
-
-
-    this.init = function () {
-        this.init_load_history_buttons();
-        this.init_export_history_buttons();
-        this.init_restore_version_buttons();
-
-        // check for the version_history fragment and open the modal if present.
-        if (this._has_version_fragment()) {
-            const first_entity = $(".caosdb-entity-panel")[0];
-            if (first_entity && hasEntityPermission(first_entity, "RETRIEVE:HISTORY")) {
-                logger.debug("Showing full version modal for first entity");
-                const version_button = $(first_entity).find(".caosdb-f-entity-version-button");
-                version_button.click();
-                const full_version_history_button = $(first_entity).find(".caosdb-f-entity-version-load-history-btn");
-                full_version_history_button.click();
-            }
-        }
-    }
-}
-
 var paging = new function () {
 
     this.defaultPageLen = 10;
@@ -1373,16 +1200,96 @@ var paging = new function () {
     }
 };
 
-var queryForm = new function () {
-    this.init = function (form) {
-        this.restoreLastQuery(form, () => window.sessionStorage.lastQuery);
-        this.bindOnClick(form, (set) => {
+/**
+ * Extend the functionality of the pure html query panel.
+ *
+ * @module queryForm
+ * @global
+ */
+var queryForm = function () {
+
+    const init = function (form) {
+        queryForm.restoreLastQuery(form, () => window.sessionStorage.lastQuery);
+        queryForm.bindOnClick(form, (set) => {
             window.sessionStorage.lastQuery = set;
-            return null;
         });
+        const BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS = "";
+        queryForm.initFreeSearch(form, `${BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS}`);
     };
 
-    this.restoreLastQuery = function (form, getter) {
+    const logger = log.getLogger("queryForm");
+    var role_name_facet_select = undefined;
+
+    const _isCql = function (query) {
+       query = query.toUpperCase().trim();
+       return (query.startsWith("FIND") || query.startsWith("COUNT") || query.startsWith("SELECT"));
+    }
+
+    /**
+     * Initialize the free search to generate search queries which search only
+     * within one of several options of roles or entity names.
+     *
+     * E.g. when `options` is "Person, Experiment, Sample" the user can select
+     * one of these options before submitting the query. When the user types in
+     * something that doesn't looks like a CQL-query, a query is generated
+     * instead which goes like: FIND Persion WHICH HAS A PROPERTY LIKE
+     * "*something*".
+     *
+     * Note: This feature is disabled by default. Enable it by specifying the
+     * build variable BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS as a
+     * comma-separated list of **properly quoted** expressions, e.g.
+     * "FILE 'numpy array', 'Plant Experiemnt', 'quotes_not_necessary'".
+     * Otherwise, the server will throw a syntax error.
+     *
+     * @function initFreeSearch
+     * @param {HTMLElement} form - the form which will be initialized for the
+     *     free search.
+     * @param {string} options - comma-separated list of options.
+     * @return {boolean} - true if the initialization was successful, false
+     *     otherwise.
+     */
+    const initFreeSearch = function (form, options) {
+        const textArea = $(form).find(".caosdb-f-query-textarea");
+        logger.trace("initFreeSearch", form, textArea, options);
+        if (textArea.length > 0 && options && options != "") {
+            const lastQuery = window.localStorage["freeTextQuery:" + textArea[0].value];
+            if (lastQuery) {
+                textArea[0].value = lastQuery;
+            }
+
+            const selected = window.localStorage["role_name_facet_option"];
+            const select = $(`<select class="btn btn-secondary"/>`);
+            for (let option of options.split(",")) {
+              select.append(`<option ${selected === option.trim() ? "selected" : ""}>${option}</option>`);
+            }
+            $(form).find(".input-group").prepend(select);
+            role_name_facet_select = select[0];
+
+            const switchFreeSearch = (text_area) => {
+                if(_isCql(text_area.value)) {
+                    select.hide();
+                    $(text_area).css({"border-top-left-radius": "0.375rem", "border-bottom-left-radius": "0.375rem"});
+                } else {
+                    select.show();
+                    $(text_area).css({});
+                }
+            }
+
+            switchFreeSearch(textArea[0]);
+
+            textArea.on("keydown", (e) => {
+                switchFreeSearch(e.target);
+            });
+            return true;
+        }
+        role_name_facet_select = undefined;
+        return false;
+    }
+
+    /**
+     * @function restoreLastQuery
+     */
+    const restoreLastQuery = function (form, getter) {
         if (form == null) {
             throw new Error("form was null");
         }
@@ -1392,10 +1299,11 @@ var queryForm = new function () {
     };
 
     /**
-     * @value {string} query - the query string.
+     * @function redirect
+     * @param {string} query - the query string.
      * @param {string} paging - the paging string, e.g. 0L10.
      */
-    this.redirect = function (query, paging) {
+    const redirect = function (query, paging) {
         var pagingparam = ""
         if (paging && paging.length > 0) {
             pagingparam = "P=" + paging + "&";
@@ -1403,6 +1311,20 @@ var queryForm = new function () {
         location.href = connection.getBasePath() + "Entity/?" + pagingparam + "query=" + query;
     }
 
+    /**
+     * Read out the selector for the role/name facet of the query. Return
+     * "RECORD" if the selector is disabled.
+     * @function getRoleNameFacet
+     */
+    const getRoleNameFacet = function () {
+        if (role_name_facet_select) {
+            const result = role_name_facet_select.value;
+            window.localStorage["role_name_facet_option"] = result;
+            return result;
+        }
+        return "RECORD";
+    }
+
     const _splitSearchTermsPattern = /"(?<dq>[^"]*)" |'(?<sq>[^']*)' |(?<nq>[^ ]+)/g;
 
     /**
@@ -1413,15 +1335,30 @@ var queryForm = new function () {
      * enclosing quotation marks are being stripped. Currently no support for
      * escape sequences for quotation marks.
      *
+     * @function splitSearchTerms
      * @param {string} query - complete query string.
      * @return {string[]} array of the search terms.
      */
-    this.splitSearchTerms = function (query) {
+    const splitSearchTerms = function (query) {
         // add empty space at the end, so every matching group ends with it -> easier regex. Also, undefined is filtered out
         return Array.from((query + " ").matchAll(_splitSearchTermsPattern), (m) => m[1] || m[2] || m[3]).filter((word) => word);
     }
 
-    this.bindOnClick = function (form, setter) {
+    /**
+     * Is the query a SELECT field,... FROM entity query?
+     *
+     * @function isSelectQuery
+     * @param {HTMLElement} query, the query to be tested.
+     * @return {Boolean}
+     */
+    const isSelectQuery = function (query) {
+        return query.toUpperCase().startsWith("SELECT");
+    }
+
+    /**
+     * @function bindOnClick
+     */
+    const bindOnClick = function (form, setter) {
         if (setter == null || typeof (setter) !== 'function' || setter.length !== 1) {
             throw new Error("setter must be a function with one param");
         }
@@ -1434,37 +1371,40 @@ var queryForm = new function () {
         var submithandler = function () {
             // store current query
             var queryField = form.query;
-            var value = queryField.value.toUpperCase().trim();
+            var value = queryField.value;
             if (typeof value == "undefined" || value.length == 0) {
                 return;
             }
-            if (!(value.startsWith("FIND") || value.startsWith("COUNT") || value.startsWith("SELECT"))) {
+            if (!_isCql(value)) {
                 // split words in query field at space and create query fragments
-                var words = queryForm.splitSearchTerms(queryField.value).map(word => `A PROPERTY LIKE '*${word.replaceAll("'", `\\'`)}*'`);
+                var words = splitSearchTerms(queryField.value).map(word => `A PROPERTY LIKE '*${word.replaceAll("'", `\\'`)}*'`);
                 if (!words.length) {
                     return false;
                 }
-                var query_string = "FIND ENTITY WHICH HAS ";
-                // send a query that combines all fragments with an AND 
-                queryField.value = query_string + words.join(" AND ");
+                const e = getRoleNameFacet();
+                const query_string = `FIND ${e} WHICH HAS ` + words.join(" AND ");
+                queryField.value = query_string;
+
+                // store original value of the text field
+                window.localStorage["freeTextQuery:" + query_string] = value;
             }
             setter(queryField.value);
 
             var paging = "";
-            if (form.P && !queryForm.isSelectQuery(queryField.value)) {
+            if (form.P && !isSelectQuery(queryField.value)) {
                 paging = form.P.value
             }
 
             queryForm.redirect(queryField.value.trim(), paging);
-        };
 
-        $("#caosdb-query-textarea").on("keydown", (e) => {
-            // prevent submit on enter
-            if (e.originalEvent.which == 13) {
-                e.originalEvent.preventDefault();
-            }
-        })
+            var btn =  $(form).find(".caosdb-search-btn");
+            btn.html(`<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`);
+            btn.prop("disabled", true);
 
+            var textField =  $(form).find(".caosdb-f-query-textarea");
+            textField.blur();
+            textField.prop("disabled", true);
+        };
 
         // handler for the form
         form.onsubmit = function (e) {
@@ -1480,31 +1420,28 @@ var queryForm = new function () {
     };
 
     /**
-     * Is the query a SELECT field,... FROM entity query?
-     *
-     * @param {HTMLElement} query, the query to be tested.
-     * @return {Boolean}
+     * @function getRoleNameFacetSelect
      */
-    this.isSelectQuery = function (query) {
-        return query.toUpperCase().startsWith("SELECT");
-    }
 
-    /**
-     * Remove the (hidden) paging input from the query form.
-     * The form is changed in-place without copying it.
-     *
-     * @param {HTMLElement} form, the query form.
-     * @return {HTMLElement} the form without the paging input.
-     */
-    this.removePagingField = function (form) {
-        $(form.P).remove();
-        return form;
+    return {
+        init: init,
+        initFreeSearch: initFreeSearch,
+        isSelectQuery: isSelectQuery,
+        restoreLastQuery: restoreLastQuery,
+        redirect: redirect,
+        bindOnClick: bindOnClick,
+        splitSearchTerms: splitSearchTerms,
+        getRoleNameFacet: getRoleNameFacet,
+        getRoleNameFacetSelect: () => role_name_facet_select,
     }
-};
+}();
 
 
-/**
+/*
  * Small module containing only a converter from markdown to html.
+ *
+ * @module markdown
+ * @global
  */
 this.markdown = new function () {
     this.dependencies = ["showdown", "caosdb_utils"];
@@ -1538,6 +1475,10 @@ this.markdown = new function () {
     });
 }
 
+/**
+ * @module hintMessages
+ * @global
+ */
 var hintMessages = new function () {
     this.init = function () {
         for (var entity of $('.caosdb-entity-panel')) {
@@ -1992,7 +1933,6 @@ function initOnDocumentReady() {
     }
     caosdb_modules.init();
     navbar.init();
-    version_history.init();
 
     if ("${BUILD_MODULE_USER_MANAGEMENT}" == "ENABLED") {
         caosdb_modules.register(user_management);
@@ -2015,6 +1955,7 @@ function initOnDocumentReady() {
  *
  * Singleton which is globally available under caosdb_modules.
  *
+ * @class _CaosDBModules
  * @property {boolean} auto_init - if modules are initialized automatically
  *   after beeing registered, or when `this.init_all()` is being called.
  */
diff --git a/src/core/webcaosdb.xsl b/src/core/webcaosdb.xsl
index 9a0d6769f1901e860c7a2318fce28957d6461496..38b6461e8793d9063f1d8f0047df9232c138da54 100644
--- a/src/core/webcaosdb.xsl
+++ b/src/core/webcaosdb.xsl
@@ -66,6 +66,14 @@
         <xsl:call-template name="caosdb-head-js" />
       </head>
       <body>
+        <xsl:choose>
+          <xsl:when test="'${BUILD_MODULE_EXT_PROPERTY_DISPLAY}'='ENABLED'">
+            <xsl:attribute name="data-hidden-properties">true</xsl:attribute>
+          </xsl:when>
+          <xsl:otherwise>
+            <xsl:attribute name="data-hidden-properties">false</xsl:attribute>
+          </xsl:otherwise>
+        </xsl:choose>
         <xsl:attribute name="data-response-count">
           <xsl:value-of select="/Response/@count"/>
         </xsl:attribute>
diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl
index 82de9e416c2e28f21cd3f386cc6e04419284dc5f..264136f09828dce4e2ba7d324ff09fc14db5842c 100644
--- a/src/core/xsl/entity.xsl
+++ b/src/core/xsl/entity.xsl
@@ -232,7 +232,15 @@
   </xsl:template>
   <!-- PROPERTIES -->
   <xsl:template match="Property" mode="entity-body">
-    <li class="list-group-item caosdb-v-property-row caosdb-f-entity-property">
+    <li>
+      <xsl:choose>
+        <xsl:when test="'${BUILD_MODULE_EXT_PROPERTY_DISPLAY}'='ENABLED'">
+          <xsl:attribute name="class">list-group-item caosdb-v-property-row caosdb-f-entity-property caosdb-v-hidden-property</xsl:attribute>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:attribute name="class">list-group-item caosdb-v-property-row caosdb-f-entity-property</xsl:attribute>
+        </xsl:otherwise>
+      </xsl:choose>
       <xsl:attribute name="id">
         <xsl:value-of select="generate-id()"/>
       </xsl:attribute>
diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl
index 702a390f28ada96e140f40f10218b1740ab10700..6497cc9c64f7921c2b26b4ccd6535d181ddfeb4b 100644
--- a/src/core/xsl/query.xsl
+++ b/src/core/xsl/query.xsl
@@ -359,7 +359,7 @@
         </xsl:attribute>
         <input id="caosdb-query-paging-input" name="P" type="hidden" value="0L10"/>
         <div class="input-group">
-          <input class="form-control" id="caosdb-query-textarea" name="query" placeholder="E.g. 'FIND Experiment'" rows="1" style="resize: vertical;" type="text"></input>
+          <input class="form-control caosdb-f-query-textarea" id="caosdb-query-textarea" name="query" placeholder="E.g. 'FIND Experiment'" rows="1" style="resize: vertical;" type="text"></input>
             <a class="btn btn-secondary caosdb-search-btn" href="#" title="Click to execute the query.">
               <i class="bi-search"></i>
             </a>
diff --git a/src/doc/conf.py b/src/doc/conf.py
index 6050977e9f455b2dcdb9102e376d1839ae93c627..cde1d8d627cd3364159ffdb899a701dd72eedd82 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -26,9 +26,9 @@ copyright = '2022, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.9.1'
+version = '0.10.2'
 # The full version, including alpha/beta/rc tags
-release = '0.9.1-SNAPSHOT'
+release = '0.10.2-SNAPSHOT'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/src/doc/extension/display_of_properties.rst b/src/doc/extension/display_of_properties.rst
new file mode 100644
index 0000000000000000000000000000000000000000..29d254c15a8409047ac6087030c4a35093db3ac5
--- /dev/null
+++ b/src/doc/extension/display_of_properties.rst
@@ -0,0 +1,140 @@
+Tweaking the display of properties
+==================================
+
+Hide or show properties for specific roles and users
+****************************************************
+
+.. note::
+
+   This feature was introduced CaosDB WebUI 0.10.
+
+.. warning::
+
+   Hiding properties is purely cosmetics and should **never** be considered a
+   security feature. The hidden properties are still communicated with the server.
+
+Sometimes it is desirable to hide certain properties for specific users or
+roles, e.g., when they might be irrelevant or confusing. For example, an
+internal id might only be of interest to curators or administrators, whereas it
+is entirely meaningless to most other users.
+
+To configure the hiding of properties, you first need to enable the build
+variable ``BUILD_MODULE_EXT_PROPERTY_DISPLAY``. Then, the display of the
+properties is configured in ``conf/ext/json/ext_prop_display.json``. In there,
+properties of a specific RecordType can be hidden by specifying the name of the
+property, and the names of the roles and/or users from whom it should be hidden.
+
+.. code-block:: json
+
+   {
+     "RecordTypeName": {
+       "hide": [
+         {
+           "name": "property name",
+           "roles": ["list", "of", "roles"],
+           "users": ["list", "of", "users"]
+         },
+         ...
+       ]
+     },
+     ...
+   }
+
+In the same way but using the ``show`` keyword, properties can be hidden for
+everyone **but** the specified users/roles:
+
+.. code-block:: json
+
+   {
+     "RecordTypeName": {
+       "show": [
+         {
+           "name": "property name",
+           "roles": ["list", "of", "roles"],
+           "users": ["list", "of", "users"]
+         },
+         ...
+       ]
+     },
+     ...
+   }
+ 
+
+For example, using the data from demo.indiscale.com, the following would hide
+the ``price`` of all ``MusicalInstruments`` from ``anonymous`` and their
+``Manufacturer`` from the ``admin`` user.
+
+.. code-block:: json
+
+   {
+     "MusicalInstrument": {
+       "hide": [
+         {"name": "price", "roles": ["anonymous"], "users": []},
+         {"name": "Manufacturer", "roles": [], "users": ["admin"]}
+       ]
+     }
+   }
+
+Defining the order of properties
+********************************
+
+Similar to above, the order in which properties are displayed can also be
+specified. Again, the build variable ``BUILD_MODULE_EXT_PROPERTY_DISPLAY`` has
+to be enabled. Then, the order in which the properties are displayed can be
+configured in the same configuration file as above, i.e., in
+``conf/ext/json/ext_prop_display.json``:
+
+.. code-block:: json
+
+   {
+     "RecordTypeName": {
+       "order": ["ordered", "list", "of", "properties"]
+     },
+     ...
+   }
+
+This ensures that the properties of all entities with the declared types are
+displayed in the defined order if present. Additional properties, that are not
+part of this list are appended in their original order as returned from the CaosDB
+server.
+
+Using again the data of demo.indiscale.com for an example, a configuration might
+look the following:
+
+.. code-block:: json
+
+   {
+     "MusicalInstrument": {
+       "order": ["price", "Manufacturer"]
+     }
+   }
+
+In all ``MusicalInstrument`` entities, the ``price`` would then be shown first,
+then the ``Manufacturer``, and then all remaining properties. If it doesn't have
+a ``price`` property, ``Manufacturer`` is shown on top.
+
+Of course, this feature can be combined with the hiding of properties:
+
+.. code-block:: json
+
+   {
+     "MusicalInstrument": {
+       "hide": [
+         {"name": "price", "roles": ["anonymous"], "users": []},
+         {"name": "Manufacturer", "roles": [], "users": ["admin"]}
+       ],
+       "order": ["price", "Manufacturer"]
+     }
+   }
+
+
+In this example, ``price`` would still be displayed on top of the list of
+property in every ``MusicalInstrument`` entity, but it is hidden for the
+``anonymous`` role.
+
+Future
+******
+
+In the future, this feature will be extended to allow to `toggle
+<https://gitlab.com/caosdb/caosdb-webui/-/issues/190>`__ properties of
+predifined RecordTypes.
diff --git a/src/doc/tutorials/edit_mode.rst b/src/doc/tutorials/edit_mode.rst
index 3e7d9ffb4472ab8ff0ec9a4539ab9ab11563ffd2..e638bb6a7a70c485acebc6d568868b2c409bbdd8 100644
--- a/src/doc/tutorials/edit_mode.rst
+++ b/src/doc/tutorials/edit_mode.rst
@@ -25,7 +25,7 @@ Edit Mode``. Unsurprisingly, clicking here terminates the edit mode.
    privileges. You can only create/edit/delete entities if your user
    is allowed to do that. User and group permissions can be configured
    in detail as explained in the `server documentation
-   <https://docs.indiscale.com/caosdb-server/Permissions.html>`_.
+   <https://docs.indiscale.com/caosdb-server/permissions.html>`_.
 
 When you have entered the edit mode, you'll see the edit mode toolbox
 appearing on the right hand side of your screen:
diff --git a/test/core/js/modules/ext_prop_display.js.js b/test/core/js/modules/ext_prop_display.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..8c2c0264b44810786ed64fc556d21fb21dbe9b1f
--- /dev/null
+++ b/test/core/js/modules/ext_prop_display.js.js
@@ -0,0 +1,324 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2022 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/>.
+ */
+
+'use strict';
+
+QUnit.module("ext_prop_display.js", {
+    before: function (assert) {
+        // setup before module
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+        // entity list, one entity with three properties (ids 1,2,3 for
+        // testing), all of them hidden by default.
+        $(document.body).append('<div class="caosdb-f-main-entities prop-display-test-entities"><div id=115 class="caosdb-entity-panel"><div class="caosdb-entity-panel-heading"><span class="caosdb-f-parent-list"><span class="caosdb-parent-item"><a class="caosdb-parent-name" href="https://demo.indiscale.com/Entity/110">Guitar</a></span></span></div><div class="caosdb-entity-panel-body"><ul class="list-group caosdb-properties"><li id=1 class="caosdb-v-property-row caosdb-f-entity-property caosdb-v-hidden-property"><div class="row"><div class="caosdb-v-property-left-col"><span class="caosdb-property-name">first prop</span></div><div class="caosdb-f-property-value"><span class="caosdb-f-property-single-raw-value caosdb-property-text-value caosdb-f-property-text-value caosdb-v-property-text-value">48.0</span><span class="caosdb-unit">€</span></div></div></li><li id=2 class="caosdb-v-property-row caosdb-f-entity-property caosdb-v-hidden-property"><div class="row"><div class="caosdb-v-property-left-col"><span class="caosdb-property-name">second prop</span></div><div class="caosdb-f-property-value"><span class="caosdb-f-property-single-raw-value caosdb-property-text-value caosdb-f-property-text-value caosdb-v-property-text-value">48.0</span><span class="caosdb-unit">€</span></div></div></li><li id=3 class="caosdb-v-property-row caosdb-f-entity-property caosdb-v-hidden-property"><div class="row"><div class="caosdb-v-property-left-col"><span class="caosdb-property-name">third prop</span></div><div class="caosdb-f-property-value"><span class="caosdb-f-property-single-raw-value caosdb-property-text-value caosdb-f-property-text-value caosdb-v-property-text-value">48.0</span><span class="caosdb-unit">€</span></div></div></li></ul></div></div></div>');
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+        $(".prop-display-test-entities").remove();
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("unhide all properties", function (assert) {
+    assert.ok(prop_display.unhideAllProperties, "unhideAllProperties available");
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all properties hidden initially");
+    prop_display.unhideAllProperties();
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 0, "no hidden properties after unhiding");
+});
+
+QUnit.test("hide properties garbage type", function (assert) {
+    assert.ok(prop_display.getEntitiesInView, "getEntitiesInView available");
+    assert.ok(prop_display.displayProperties, "displayProperties available");
+    const conf = {
+        "DoesntExist": {
+            "hide": [{
+                "name": "first prop",
+                "roles": ["some_role"],
+                "users": ["someone"]
+            }]
+        }
+    };
+    // only one garbage type
+    const allTypes = {
+        "typesWithChildren": {
+            "DoesntExist": ["DoesntExist"]
+        },
+        "allTypesOrChildren": ["DoesntExist"]
+    };
+    const userName = "someone";
+    const userRoles = ["some_role", "some_other_role"];
+    const entities = prop_display.getEntitiesInView();
+    assert.equal(entities.length, 1, "only one entity in test data");
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all properties hidden initially");
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 0, "no garbage-type entity, so no hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 3, "all properties are being shown");
+});
+
+QUnit.test("hide properties garbage property", function (assert) {
+    assert.ok(prop_display.getEntitiesInView, "getEntitiesInView available");
+    assert.ok(prop_display.displayProperties, "displayProperties available");
+    const conf = {
+        "MusicalInstrument": {
+            "hide": [{
+                "name": "prop does not exist",
+                "roles": ["some_role"],
+                "users": ["someone"]
+            }]
+        }
+    };
+    const allTypes = {
+        "typesWithChildren": {
+            "MusicalInstrument": ["MusicalInstrument", "Guitar"]
+        },
+        "allTypesOrChildren": ["MusicalInstrument", "Guitar"]
+    };
+    const userName = "someone";
+    const userRoles = ["some_role", "some_other_role"];
+    const entities = prop_display.getEntitiesInView();
+    assert.equal(entities.length, 1, "only one entity in test data");
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all properties hidden initially");
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 0, "no garbage property, so no hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 3, "all properties are being shown");
+});
+
+
+QUnit.test("hide properties", function (assert) {
+    assert.ok(prop_display.getEntitiesInView, "getEntitiesInView available");
+    assert.ok(prop_display.displayProperties, "displayProperties available");
+    const conf = {
+        "MusicalInstrument": {
+            "hide": [{
+                    "name": "first prop",
+                    "roles": ["some_role"],
+                    "users": ["someone"]
+                },
+                {
+                    "name": "second prop",
+                    "roles": [],
+                    "users": ["someone else"]
+                },
+                {
+                    "name": "third prop",
+                    "roles": ["some_other_role"],
+                    "users": ["someone else"]
+                }
+            ]
+        }
+    };
+    const allTypes = {
+        "typesWithChildren": {
+            "MusicalInstrument": ["MusicalInstrument", "Guitar"]
+        },
+        "allTypesOrChildren": ["MusicalInstrument", "Guitar"]
+    };
+    var userName = "someone";
+    var userRoles = ["some_role"];
+    const entities = prop_display.getEntitiesInView();
+    assert.equal(entities.length, 1, "only one entity in test data");
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all properties hidden initially");
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 1, "exactly one hidden property");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 2, "the remaining two are shown");
+    assert.equal($("#1").hasClass("caosdb-v-hidden-property"), true, "first prop hidden");
+    assert.equal($("#2").hasClass("caosdb-v-show-property"), true, "second prop shown");
+    assert.equal($("#3").hasClass("caosdb-v-show-property"), true, "third prop shown");
+
+    // reset
+    prop_display.displayProperties(entities, conf, allTypes, "", []);
+    assert.equal($(document).find(".caosdb-v-show-property").length, 3, "all shown after reset");
+
+    userRoles = ["some_other_role"];
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 2, "two hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 1, "the remaining one is shown");
+    assert.equal($("#1").hasClass("caosdb-v-hidden-property"), true, "first prop hidden");
+    assert.equal($("#2").hasClass("caosdb-v-show-property"), true, "second prop shown");
+    assert.equal($("#3").hasClass("caosdb-v-hidden-property"), true, "third prop hidden");
+
+    // reset
+    prop_display.displayProperties(entities, conf, allTypes, "", []);
+    assert.equal($(document).find(".caosdb-v-show-property").length, 3, "all shown after reset");
+
+    userName = "someone else";
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 2, "two hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 1, "the remaining one is shown");
+    assert.equal($("#1").hasClass("caosdb-v-show-property"), true, "first prop shown");
+    assert.equal($("#2").hasClass("caosdb-v-hidden-property"), true, "second prop hidden");
+    assert.equal($("#3").hasClass("caosdb-v-hidden-property"), true, "third prop hidden");
+
+    // reset
+    prop_display.displayProperties(entities, conf, allTypes, "", []);
+    assert.equal($(document).find(".caosdb-v-show-property").length, 3, "all shown after reset");
+
+    userRoles = ["some_role", "some_other_role"]
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "two hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 0, "None is shown");
+    assert.equal($("#1").hasClass("caosdb-v-hidden-property"), true, "first prop hidden");
+    assert.equal($("#2").hasClass("caosdb-v-hidden-property"), true, "second prop hidden");
+    assert.equal($("#3").hasClass("caosdb-v-hidden-property"), true, "third prop hidden");
+
+});
+
+QUnit.test("show properties", function (assert) {
+    assert.ok(prop_display.getEntitiesInView, "getEntitiesInView available");
+    assert.ok(prop_display.displayProperties, "displayProperties available");
+    const conf = {
+        "MusicalInstrument": {
+            "show": [{
+                    "name": "first prop",
+                    "roles": ["some_role"],
+                    "users": ["someone"]
+                },
+                {
+                    "name": "second prop",
+                    "roles": [],
+                    "users": ["someone else"]
+                },
+                {
+                    "name": "third prop",
+                    "roles": ["some_other_role"],
+                    "users": ["someone else"]
+                }
+            ]
+        }
+    };
+    const allTypes = {
+        "typesWithChildren": {
+            "MusicalInstrument": ["MusicalInstrument", "Guitar"]
+        },
+        "allTypesOrChildren": ["MusicalInstrument", "Guitar"]
+    };
+    var userName = "someone";
+    var userRoles = ["some_role"];
+    const entities = prop_display.getEntitiesInView();
+    assert.equal(entities.length, 1, "only one entity in test data");
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all properties hidden initially");
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 2, "two hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 1, "the remaining one shown");
+    assert.equal($("#1").hasClass("caosdb-v-show-property"), true, "first prop shown");
+    assert.equal($("#2").hasClass("caosdb-v-hidden-property"), true, "second prop hidden");
+    assert.equal($("#3").hasClass("caosdb-v-hidden-property"), true, "third prop hidden");
+
+    // reset
+    prop_display.displayProperties(entities, conf, allTypes, "", []);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all hidden after reset");
+
+    userRoles = ["some_other_role"];
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 1, "one hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 2, "the remaining two are shown");
+    assert.equal($("#1").hasClass("caosdb-v-show-property"), true, "first prop shown");
+    assert.equal($("#2").hasClass("caosdb-v-hidden-property"), true, "second prop hidden");
+    assert.equal($("#3").hasClass("caosdb-v-show-property"), true, "third prop shown");
+
+    // reset
+    prop_display.displayProperties(entities, conf, allTypes, "", []);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all hidden after reset");
+
+    userName = "someone else";
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 1, "one hidden property");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 2, "the remaining ones are shown");
+    assert.equal($("#1").hasClass("caosdb-v-hidden-property"), true, "first prop hidden");
+    assert.equal($("#2").hasClass("caosdb-v-show-property"), true, "second prop shown");
+    assert.equal($("#3").hasClass("caosdb-v-show-property"), true, "third prop shown");
+
+    // reset
+    prop_display.displayProperties(entities, conf, allTypes, "", []);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 3, "all hidden after reset");
+
+    userRoles = ["some_role", "some_other_role"]
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    assert.equal($(document).find(".caosdb-v-hidden-property").length, 0, "no hidden properties");
+    assert.equal($(document).find(".caosdb-v-show-property").length, 3, "All are shown");
+    assert.equal($("#1").hasClass("caosdb-v-show-property"), true, "first prop shown");
+    assert.equal($("#2").hasClass("caosdb-v-show-property"), true, "second prop shown");
+    assert.equal($("#3").hasClass("caosdb-v-show-property"), true, "third prop shown");
+
+});
+
+QUnit.test("Sort properties", function (assert) {
+    assert.ok(prop_display.getEntitiesInView, "getEntitiesInView available");
+    assert.ok(prop_display.displayProperties, "displayProperties available");
+
+    var conf = {
+        "MusicalInstrument": {
+            "order": ["third prop", "first prop", "second prop"]
+        }
+    };
+    const allTypes = {
+        "typesWithChildren": {
+            "MusicalInstrument": ["MusicalInstrument", "Guitar"]
+        },
+        "allTypesOrChildren": ["MusicalInstrument", "Guitar"]
+    };
+    // username and roles don't matter for sorting
+    const userName = "";
+    const userRoles = [];
+    // initial order
+    var properties = $(document).find(".caosdb-v-property-row");
+    assert.equal(properties.index($("#1")), 0, "first prop at first position");
+    assert.equal(properties.index($("#2")), 1, "second prop at second position");
+    assert.equal(properties.index($("#3")), 2, "third prop at third position");
+
+    var entities = prop_display.getEntitiesInView();
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    properties = $(document).find(".caosdb-v-property-row");
+    assert.equal(properties.index($("#1")), 1, "first prop at second position");
+    assert.equal(properties.index($("#2")), 2, "second prop at third position");
+    assert.equal(properties.index($("#3")), 0, "third prop at first position");
+
+    // only specify first prop, the rest is appended in the previous order.
+    conf = {
+        "MusicalInstrument": {
+            "order": ["first prop"]
+        }
+    };
+    entities = prop_display.getEntitiesInView();
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    properties = $(document).find(".caosdb-v-property-row");
+    assert.equal(properties.index($("#1")), 0, "first prop at first position");
+    assert.equal(properties.index($("#2")), 2, "second prop at third position");
+    assert.equal(properties.index($("#3")), 1, "third prop at second position");
+
+    // two specified, the remaining prop is appended
+    conf = {
+        "MusicalInstrument": {
+            "order": ["second prop", "first prop"]
+        }
+    };
+    entities = prop_display.getEntitiesInView();
+    prop_display.displayProperties(entities, conf, allTypes, userName, userRoles);
+    properties = $(document).find(".caosdb-v-property-row");
+    assert.equal(properties.index($("#1")), 1, "first prop at second position");
+    assert.equal(properties.index($("#2")), 0, "second prop at first position");
+    assert.equal(properties.index($("#3")), 2, "third prop at third position");
+
+
+
+});
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 328a057f19af953399e08867085f6e04b5e785c5..534dc86c485ccfac184bb05bac17d595addaa3d6 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -1111,19 +1111,10 @@ QUnit.test("initEntity", function (assert) {
 QUnit.module("webcaosdb.js - queryForm", {
     before: function (assert) {
         assert.ok(queryForm, "queryForm is defined");
+        assert.notOk(queryForm.initFreeSearch(), "free search reset");
     }
 });
 
-QUnit.test("removePagingField", function (assert) {
-    assert.ok(queryForm.removePagingField, "function available.");
-    assert.throws(() => queryForm.removePagingField(), "null param throws.");
-    let form = $('<form><input name="P"></form>')[0];
-    assert.ok(form.P, "before: paging available.");
-    queryForm.removePagingField(form);
-    assert.notOk(form.P, "after: paging removed.");
-
-});
-
 QUnit.test("isSelectQuery", function (assert) {
     assert.ok(queryForm.isSelectQuery, "function available.");
     assert.throws(() => queryForm.isSelectQuery(), "null param throws.");
@@ -1196,14 +1187,14 @@ QUnit.test("bindOnClick", function (assert) {
     assert.equal(storage(), undefined, "before2: storage empty.");
     form.getElementsByClassName("caosdb-search-btn")[0].onclick();
 
-    assert.equal(storage(), "FIND ENTITY WHICH HAS A PROPERTY LIKE '*freetext*'", "after2: storage not empty.");
+    assert.equal(storage(), "FIND RECORD WHICH HAS A PROPERTY LIKE '*freetext*'", "after2: storage not empty.");
 
     // test the form submit handler analogously
     form.query.value = "freetext2";
     $("body").append(form);
     $(form).append(submitButton);
     submitButton.click();
-    assert.equal(storage(), "FIND ENTITY WHICH HAS A PROPERTY LIKE '*freetext2*'", "after3: storage not empty.");
+    assert.equal(storage(), "FIND RECORD WHICH HAS A PROPERTY LIKE '*freetext2*'", "after3: storage not empty.");
 
     $(form).remove();
 
@@ -1212,7 +1203,7 @@ QUnit.test("bindOnClick", function (assert) {
     $("body").append(form);
     $(form).append(submitButton);
     submitButton.click();
-    assert.equal(storage(), "FIND ENTITY WHICH HAS A PROPERTY LIKE '*free*' AND A PROPERTY LIKE '*text*' AND A PROPERTY LIKE '*3*'", "after4: storage not empty.");
+    assert.equal(storage(), "FIND RECORD WHICH HAS A PROPERTY LIKE '*free*' AND A PROPERTY LIKE '*text*' AND A PROPERTY LIKE '*3*'", "after4: storage not empty.");
 
     $(form).remove();
 
@@ -1221,7 +1212,7 @@ QUnit.test("bindOnClick", function (assert) {
     $("body").append(form);
     $(form).append(submitButton);
     submitButton.click();
-  assert.equal(storage(), `FIND ENTITY WHICH HAS A PROPERTY LIKE '*with double*' AND A PROPERTY LIKE '*and single*' AND A PROPERTY LIKE '*what\\'s wrong?*' AND A PROPERTY LIKE '*\\'*' AND A PROPERTY LIKE '*nothin\\'*' AND A PROPERTY LIKE '*"\\'bla*'`, "after5: stuff with quotation marks");
+  assert.equal(storage(), `FIND RECORD WHICH HAS A PROPERTY LIKE '*with double*' AND A PROPERTY LIKE '*and single*' AND A PROPERTY LIKE '*what\\'s wrong?*' AND A PROPERTY LIKE '*\\'*' AND A PROPERTY LIKE '*nothin\\'*' AND A PROPERTY LIKE '*"\\'bla*'`, "after5: stuff with quotation marks");
 
     // ... then with empty quotation marks. this will not trigger the query execution at all
     storage("not triggered");
@@ -1251,6 +1242,42 @@ QUnit.test("splitSearchTerms", function (assert) {
     }
 });
 
+QUnit.test("initFreeSearch", function (assert) {
+    const form = $('<form></form>');
+    const inputGroup = $('<div class="input-group"></div>');
+    const textArea = $( '<textarea class="caosdb-f-query-textarea" name="query"></textarea>');
+    const submitButton = $('<input class="caosdb-search-btn" type="submit">');
+    inputGroup.append(textArea);
+    form.append(inputGroup).append(submitButton);
+    $("body").append(form);
+
+    // without initialization
+    assert.ok(queryForm.initFreeSearch, "available");
+    assert.notOk(queryForm.getRoleNameFacetSelect(), "role_name_facet_select is undefined 1");
+    assert.notOk(queryForm.initFreeSearch(), "not initialized");
+
+    assert.notOk(queryForm.getRoleNameFacetSelect(), "role_name_facet_select is undefined 2");
+
+    assert.equal(form.find("select").length, 0, "no select present");
+    assert.equal(queryForm.getRoleNameFacet(), "RECORD");
+
+
+    window.localStorage["role_name_facet_option"] = "Sample";
+    assert.ok(queryForm.initFreeSearch(form, "Person, Experiment, Sample"), "initialized");
+
+    // after initialization
+    assert.ok(queryForm.getRoleNameFacetSelect(), "role_name_facet_select is defined");
+    assert.equal(queryForm.getRoleNameFacetSelect().tagName, "SELECT", "role_name_facet_select is SELECT");
+    assert.equal(form.find("select").length, 1, "select is present");
+
+    assert.equal(queryForm.getRoleNameFacet(), "Sample", "previously selected option is selected");
+    form.find("select")[0].value = "Experiment";
+    assert.equal(queryForm.getRoleNameFacet(), "Experiment");
+
+    // clean up
+    form.remove();
+})
+
 /* MODULE paging */
 QUnit.module("webcaosdb.js - paging", {
     before: function (assert) {}