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/CHANGELOG.md b/CHANGELOG.md index 1c5eb8d6970bf588fb5542e4aeb622b43e25de95..02295dbb7ee91a930a15b7ce318a167358d47383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ 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). +## [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/DEPENDENCIES.md b/DEPENDENCIES.md index 9c6f7e4c7880c488ec116662c2ed49e2bd85d8a1..49d5f792e8cd17bf6ccf4c9402686e0c557919ce 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -1,4 +1,4 @@ -* CaosDB Server 0.8.0 +* CaosDB Server 0.8.1 * 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/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_map.js b/src/core/js/ext_map.js index bac14e71f68561df6982f94fca9c5e9f02e8c90e..6dd9b963d4caa2887f4e680285e7b61a47e97b20 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -25,7 +25,7 @@ /** * @module caosdb_map - * @version 0.4.1 + * @version 0.5.0 * * For displaying a geographical map which shows entities at their associated * geolocation. @@ -43,7 +43,7 @@ var caosdb_map = new function () { var logger = log.getLogger("caosdb_map"); - this.version = "0.4.1"; + this.version = "0.5.0"; this.dependencies = ["log", { "L": ["latlngGraticule", "Proj"] }, "navbar", "caosdb_utils"]; @@ -76,6 +76,8 @@ var caosdb_map = new function () { * @property {DataModelConfig} datamodel - the data model for the * display of entities in the map (also used by the query generator). * @property {SelectConfig} select - config for the query generator. + * @property {Object.<string, EntityLayerConfig>} entityLayers - + * configuration for the entity layer which are to be shown on the map. */ /** @@ -382,14 +384,23 @@ var caosdb_map = new function () { */ /** + * Configuration of the entity layers which are to be shown on the map. + * * @typedef {object} EntityLayerConfig - * @property {string} id - * @property {string} name - * @property {string} description - * @property {DivIcon_options} icon_options + * @property {string} id - the id of the entity layer which is used + * internally. + * @property {string} name - a short name which is shown in the entity + * layers menu. + * @property {string} description - a short description which is shown as + * hover-over text in the entity layers menu. + * @property {DivIcon_options} icon_tions - leaflet options for the icon + * (aka the marker) which is shown on the map. These options are + * espcially useful to style the icons (color etc). * @property {number} zIndexOffset - * @property {mapEntityGetter} get_entities - * @property {mapEntityPopupGenerator} make_popup + * @property {mapEntityGetter} get_entities - returns the entities which + * are to be shown on the map. + * @property {mapEntityPopupGenerator} make_popup - returns the popup which + * is to be shown when a user clicks on the map marker. */ /** @@ -647,13 +658,13 @@ var caosdb_map = new function () { * "all_map_entities" which shows all entities in the database with * coordinates. * - * @type {EntityLayerConfig[]} + * @type {Object.<string, EntityLayerConfig>} */ this._default_entity_layer_config = { "current_page_entities": { "name": "Entities on the current page.", "description": "Show all entities on the current page.", - "icon": { + "icon_options": { html: '<i class="bi-geo-alt-fill" style="font-size: 20px; color: #00F;"></i>', iconAnchor: [10, 19], className: "", @@ -675,7 +686,7 @@ var caosdb_map = new function () { "all_map_entities": { "name": "All entities", "description": "Show all entities with coordinates.", - "icon": { + "icon_options": { html: '<i class="bi-geo-alt-fill" style="font-size: 20px; color: #F00;"></i>', iconAnchor: [10, 19], className: "", @@ -973,9 +984,10 @@ var caosdb_map = new function () { this._reload_layers = function () { caosdb_map._show_load_info() const promises = [] + const entity_layer_config = $.extend(true, {}, caosdb_map._default_entity_layer_config, caosdb_map.config["entityLayers"]); for (const layer of caosdb_map.layers) { promises.push(caosdb_map._fill_layer(layer.layer_group, - caosdb_map._default_entity_layer_config[layer.id])); + entity_layer_config[layer.id])); } Promise.all(promises).then((val) => { caosdb_map._hide_load_info() @@ -1067,14 +1079,16 @@ var caosdb_map = new function () { view_config); // init entity layers - this.layers = this.init_entity_layers(this._default_entity_layer_config); + const entity_layer_config = $.extend(true, {}, this._default_entity_layer_config, config["entityLayers"]); + caosdb_map.entityLayers = entity_layer_config; + this.layers = this.init_entity_layers(entity_layer_config); var layerControl = L.control.layers(); const promises = [] for (const layer of this.layers) { promises.push(caosdb_map._fill_layer(layer.layer_group, - this._default_entity_layer_config[layer.id])); + entity_layer_config[layer.id])); layerControl.addOverlay(layer.layer_group, layer.chooser_html.outerHTML); layer.layer_group.addTo(this._map); } @@ -1139,7 +1153,7 @@ var caosdb_map = new function () { */ /** - * @param {EntityLayerConfig[]} configs + * @param {Object.<string, EntityLayerConfig>} config * @returns {_EntityLayer[]} */ this.init_entity_layers = function (configs) { @@ -1165,7 +1179,7 @@ var caosdb_map = new function () { layer_group.entities = entities; var markers = caosdb_map.create_entity_markers( entities, config.datamodel, config.make_popup, - config.zIndexOffset, config.icon); + config.zIndexOffset, config.icon_options); for (const marker of markers) { layer_group.addLayer(marker); @@ -1202,7 +1216,7 @@ var caosdb_map = new function () { this.make_layer_chooser_html = function (config) { return $('<span/>') .attr("title", config.description) - .append(config.icon.html) + .append(config.icon_options.html) .append(config.name)[0]; } diff --git a/src/core/js/ext_prop_display.js b/src/core/js/ext_prop_display.js new file mode 100644 index 0000000000000000000000000000000000000000..f74d700078b2b8ab69174ab05c9e95c6df33b109 --- /dev/null +++ b/src/core/js/ext_prop_display.js @@ -0,0 +1,237 @@ +/* + * 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'; + +/** + * @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, 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); + + } 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); + } + } +}($, edit_mode, getEntityName, getEntityRole, getPropertyElements, getPropertyName, getUserName, getUserRoles, log.getLogger("ext_prop_display"), load_config, 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 48707c8b6481453457a2712658d8bf2085703be3..9c8fb8b0efdeb765156b1b6ab92b9cafea6669a1 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.0' +version = '0.10.0' # The full version, including alpha/beta/rc tags -release = '0.9.0' +release = '0.10.0' # -- 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..591ce9b2a07f84eb20fe12a2128f47858103e266 --- /dev/null +++ b/src/doc/extension/display_of_properties.rst @@ -0,0 +1,142 @@ +Tweaking the display of properties +================================== + +Hide or show properties for specific roles and users +**************************************************** + +.. note:: + + This feature is part of CaosDB WebUI 0.10 and is not available for 0.9.X or + older. + +.. warning:: + + Hiding properties is purely cosmetics and should **never** be considered a + security feature. The hidden properties are still part of the server + response. + +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/extension/query_templates.rst b/src/doc/extension/query_templates.rst index 015d26a21fe20bc09dc97db9c7a3e7a1ca58a0b3..4e511ac9f91b26ffcda2674362b00070948c1bd2 100644 --- a/src/doc/extension/query_templates.rst +++ b/src/doc/extension/query_templates.rst @@ -192,7 +192,7 @@ The following example for the file global_query_shortcuts.json would create two { "description": "Show a table of Experiments for year: {year}", "query": "SELECT date, project, identifier FROM Record Experiment with date in {year}" - }, + } ] Data Model for User Query Templates diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 3e55506b9abc8742ae59bf958febd9f63f968ba9..54c8d003029632f13277c05c332b0c4993c35c63 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -76,7 +76,7 @@ QUnit.module("ext_map.js", { }); QUnit.test("availability", function (assert) { - assert.equal(caosdb_map.version, "0.4.1", "test version"); + assert.equal(caosdb_map.version, "0.5.0", "test version"); assert.ok(caosdb_map.init, "init available"); }); @@ -272,7 +272,7 @@ QUnit.test("make_layer_chooser_html", function (assert) { "id": "test_id", "name": "test name", "description": "test description", - "icon": { + "icon_options": { "html": "<span>ICON</span>", }, }; @@ -287,7 +287,7 @@ QUnit.test("_init_single_entity_layer", function (assert) { "id": "test_id", "name": "test name", "description": "test description", - "icon": { + "icon_options": { "html": "<span>ICON</span>", }, } 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) {}