diff --git a/CHANGELOG.md b/CHANGELOG.md index 1456f1850733a6f9f9d5d286634ee32d4639b504..1f1b0ed001204cb31f8f836f475c5c63bf426c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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. + ### 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. ### Deprecated diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 078aed9b54caca678112ae443965d076c2b96915..9b8f8095befc01f3f9fba610ee61efbaa8c24fc5 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -76,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 ############################################################################## @@ -122,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 @@ -129,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 @@ -147,6 +170,7 @@ MODULE_DEPENDENCIES=( webcaosdb.js pako.js utif.js + ext_version_history.js caosdb.js form_elements.js ext_autocomplete.js 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_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/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/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) {}