diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 9b8f8095befc01f3f9fba610ee61efbaa8c24fc5..f188535c2e33448dc46297f2333050d2b480f96f 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -54,6 +54,7 @@ BUILD_MODULE_EXT_ANNOTATION=ENABLED BUILD_MODULE_EXT_COSMETICS_LINKIFY=DISABLED BUILD_MODULE_EXT_QRCODE=ENABLED BUILD_MODULE_SHOW_ID_IN_LABEL=DISABLED +BUILD_MODULE_LEGACY_QUERY_FORM=DISABLED BUILD_MODULE_USER_MANAGEMENT=ENABLED BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB @@ -168,6 +169,7 @@ MODULE_DEPENDENCIES=( loglevel.js plotly.js webcaosdb.js + query_form.js pako.js utif.js ext_version_history.js diff --git a/src/core/js/query_form.js b/src/core/js/query_form.js new file mode 100644 index 0000000000000000000000000000000000000000..89056c13dd9f82f8fa9781d5a1676e9616b7714c --- /dev/null +++ b/src/core/js/query_form.js @@ -0,0 +1,245 @@ +/** + * 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; + }); + const BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS = ""; + queryForm.initFreeSearch(form, `${BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS}`); + }; + + 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"); + } + if (getter()) { + form.query.value = getter(); + } + }; + + /** + * @function redirect + * @param {string} query - the query string. + * @param {string} paging - the paging string, e.g. 0L10. + */ + const redirect = function (query, paging) { + var pagingparam = "" + if (paging && paging.length > 0) { + pagingparam = "P=" + paging + "&"; + } + 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; + + /** + * Split a query string into single terms. + * + * Terms are separated by white spaces. Terms which contain white spaces + * which are to be preserved must be enclosed in " or ' quotes. The + * 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. + */ + 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); + } + + /** + * 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"); + } + + /* + Here a submit handler is created that is attached to both the form submit handler + and the click handler of the button. + See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary. + */ + var submithandler = function () { + // store current query + var queryField = form.query; + var value = queryField.value; + if (typeof value == "undefined" || value.length == 0) { + return; + } + if (!_isCql(value)) { + // split words in query field at space and create query fragments + var words = splitSearchTerms(queryField.value).map(word => `A PROPERTY LIKE '*${word.replaceAll("'", `\\'`)}*'`); + if (!words.length) { + return false; + } + 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 && !isSelectQuery(queryField.value)) { + paging = form.P.value + } + + queryForm.redirect(queryField.value.trim(), paging); + + 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) { + e.preventDefault(); + submithandler(); + return false; + }; + + // same handler for the button + form.getElementsByClassName("caosdb-search-btn")[0].onclick = function () { + submithandler(); + }; + }; + + /** + * @function getRoleNameFacetSelect + */ + + return { + init: init, + initFreeSearch: initFreeSearch, + isSelectQuery: isSelectQuery, + restoreLastQuery: restoreLastQuery, + redirect: redirect, + bindOnClick: bindOnClick, + splitSearchTerms: splitSearchTerms, + getRoleNameFacet: getRoleNameFacet, + getRoleNameFacetSelect: () => role_name_facet_select, + } +}(); + + +$(document).ready(function () { + if (`${BUILD_MODULE_LEGACY_QUERY_FORM}` == "ENABLED") { + var form = document.getElementById("caosdb-query-form"); + if (form != null) { + queryForm.init(form); + } + } +}); diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index bfe0eb35c8e72e6493ad273ae0ed5a727019c628..18387779cfe8a9ee2ac34a1b3e69fa3f1127689f 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -1200,242 +1200,6 @@ var paging = new function () { } }; -/** - * 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; - }); - const BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS = ""; - queryForm.initFreeSearch(form, `${BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS}`); - }; - - 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"); - } - if (getter()) { - form.query.value = getter(); - } - }; - - /** - * @function redirect - * @param {string} query - the query string. - * @param {string} paging - the paging string, e.g. 0L10. - */ - const redirect = function (query, paging) { - var pagingparam = "" - if (paging && paging.length > 0) { - pagingparam = "P=" + paging + "&"; - } - 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; - - /** - * Split a query string into single terms. - * - * Terms are separated by white spaces. Terms which contain white spaces - * which are to be preserved must be enclosed in " or ' quotes. The - * 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. - */ - 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); - } - - /** - * 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"); - } - - /* - Here a submit handler is created that is attached to both the form submit handler - and the click handler of the button. - See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary. - */ - var submithandler = function () { - // store current query - var queryField = form.query; - var value = queryField.value; - if (typeof value == "undefined" || value.length == 0) { - return; - } - if (!_isCql(value)) { - // split words in query field at space and create query fragments - var words = splitSearchTerms(queryField.value).map(word => `A PROPERTY LIKE '*${word.replaceAll("'", `\\'`)}*'`); - if (!words.length) { - return false; - } - 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 && !isSelectQuery(queryField.value)) { - paging = form.P.value - } - - queryForm.redirect(queryField.value.trim(), paging); - - 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) { - e.preventDefault(); - submithandler(); - return false; - }; - - // same handler for the button - form.getElementsByClassName("caosdb-search-btn")[0].onclick = function () { - submithandler(); - }; - }; - - /** - * @function getRoleNameFacetSelect - */ - - 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. @@ -1912,12 +1676,6 @@ function initOnDocumentReady() { paging.init(); hintMessages.init(); - // init query form - var form = document.getElementById("caosdb-query-form"); - if (form != null) { - queryForm.init(form); - } - // show image 100% width $(".entity-image-preview").click(function () { $(this).css('max-width', '100%');