diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 62b1b65ab7619e963093602886f17335bbd6295a..0a370664344017060df14390de16859b03b657c5 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -52,7 +52,6 @@ BUILD_MODULE_EXT_BOOKMARKS=ENABLED BUILD_MODULE_EXT_ADD_QUERY_TO_BOOKMARKS=DISABLED BUILD_MODULE_EXT_ANNOTATION=ENABLED BUILD_MODULE_EXT_COSMETICS_LINKIFY=DISABLED -BUILD_MODULE_EXT_COSMETICS_CUSTOMDATETIME=DISABLED BUILD_MODULE_EXT_QRCODE=ENABLED BUILD_MODULE_SHOW_ID_IN_LABEL=DISABLED BUILD_MODULE_LEGACY_QUERY_FORM=DISABLED @@ -174,7 +173,6 @@ 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/ext_cosmetics.js b/src/core/js/ext_cosmetics.js index c29f07e42294ff3d8c0b4b86da829865b8a650c5..77556437394df6a6763661ce5c0d5001f68ce61a 100644 --- a/src/core/js/ext_cosmetics.js +++ b/src/core/js/ext_cosmetics.js @@ -1,10 +1,8 @@ /* * This file is a part of the CaosDB Project. * - * Copyright (C) 2021-2023 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> - * Copyright (C) 2023 Florian Spreckelsen <f.spreckelsen@indiscale.com> - * Copyright (C) 2023 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 @@ -32,38 +30,14 @@ var cosmetics = new function () { /** * Cut-off length of links. When linkify processes the links any href - * longer than this will be cut off at the end and "[...]" will be + * longer than this will be cut off at character 25 and "[...]" will be * appended for the link text. */ var _link_cut_off_length = 40; - var _custom_datetime = function () { - $('.caosdb-f-property-datetime-value').each(function (index) { - if (!($(this).hasClass("caosdb-v-property-datetime-customized"))) { - var result = this.innerText.replace(/T/, " ").replace(/\+.*/, ""); - result = `<span class="caosdb-v-property-datetime-customized-newvalue">${result}</span>`; - - // add class to highlight that this has been customized already - $(this).addClass("caosdb-v-property-datetime-customized") - $(this).hide(); - $(this).after(result); - } - }); - } - - /** - * Remove all the custom datetime elements again, for example when entering the edit mode. - */ - var _custom_datetime_clear = function() { - $('.caosdb-v-property-datetime-customized-newvalue').each(function () { - $(this).remove(); - } - ) - } - var _linkify = function () { $('.caosdb-f-property-text-value').each(function (index) { - if (!($(this).hasClass("caosdb-v-property-linkified")) && (/https?:\/\//.test(this.innerText))) { + if (/https?:\/\//.test(this.innerText)) { var result = this.innerText.replace(/https?:\/\/[^\s]*/g, function (href, index) { var link_text = href; if (_link_cut_off_length > 4 && link_text.length > _link_cut_off_length) { @@ -73,31 +47,12 @@ var cosmetics = new function () { return `<a title="Open ${href} in a new tab." target="_blank" class="caosdb-v-property-href-value" href="${href}">${link_text} <i class="bi bi-box-arrow-up-right"></i></a>`; }); - // add class to highlight that this has been linkified already - // (see https://gitlab.com/caosdb/caosdb-webui/-/issues/199). - $(this).addClass("caosdb-v-property-linkified") $(this).hide(); $(this).after(result); } }); } - /** - * Customize datetime formatting. - * - * A listener detects edit-mode changes and previews - */ - var custom_datetime = function () { - _custom_datetime(); - - // edit-mode-listener to delete replacement - document.body.addEventListener(edit_mode.start_edit.type, _custom_datetime_clear, true); - // edit-mode-listener to recreate - document.body.addEventListener(edit_mode.end_edit.type, _custom_datetime, true); - // preview listener - document.body.addEventListener(preview.previewReadyEvent.type, _custom_datetime, true); - } - /** * Convert any substring of a text-value beginning with 'http(s)://' into a * link. @@ -114,10 +69,6 @@ var cosmetics = new function () { } this.init = function () { - this.custom_datetime = custom_datetime; - if ("${BUILD_MODULE_EXT_COSMETICS_CUSTOMDATETIME}" == "ENABLED") { - custom_datetime(); - } this.linkify = linkify; if ("${BUILD_MODULE_EXT_COSMETICS_LINKIFY}" == "ENABLED") { linkify(); @@ -129,4 +80,4 @@ var cosmetics = new function () { $(document).ready(function () { caosdb_modules.register(cosmetics); -}); +}); \ No newline at end of file diff --git a/src/core/js/ext_entity_acl.js b/src/core/js/ext_entity_acl.js index 9fef31fac6f4ff27a4da12f4019d49b0e258f87d..ce6d4160afefffa11a346804a565f832a0aca84e 100644 --- a/src/core/js/ext_entity_acl.js +++ b/src/core/js/ext_entity_acl.js @@ -32,15 +32,10 @@ * * BUILD_MODULE_EXT_ENTITY_ACL_URI_ROOT=[scheme://host:port]/what/evs * - * Enable/disable the creation of a user-management link that is shown to - * administrators with - * - * BUILD_MODULE_EXT_ENTITY_ACL_USER_MANAGEMENT_BUTTON=ENABLED - * * * @author Timm Fitschen */ -var ext_entity_acl = function ($, connection, getEntityVersion, getEntityID, logger, navbar, userIsAdministrator) { +var ext_entity_acl = function ($, connection, getEntityVersion, getEntityID, logger) { const BUILD_MODULE_EXT_ENTITY_ACL_URI_ROOT = connection.getBasePath() + "webinterface/acm/entityacl/"; const _buttons_list_class = "caosdb-v-entity-header-buttons-list"; @@ -74,14 +69,6 @@ var ext_entity_acl = function ($, connection, getEntityVersion, getEntityID, log $(entity).find(`.${_buttons_list_class} .${_entity_acl_link_class}`).remove(); } - var showUserManagementLink = function () { - if (userIsAdministrator()) { - navbar.add_button($('<a class="nav-link" href="/webinterface/acm/">User Administration</a>')[0], { - title: "Go to user administration" - }); - } - } - var _init = function () { for (let entity of $(".caosdb-entity-panel")) { remove_entity_acl_link(entity); @@ -95,10 +82,6 @@ var ext_entity_acl = function ($, connection, getEntityVersion, getEntityID, log * Removes all respective buttons if present before adding a new one. */ var init = function () { - if ("${BUILD_MODULE_EXT_ENTITY_ACL_USER_MANAGEMENT_BUTTON}" == "ENABLED") { - showUserManagementLink(); - } - _init(); // edit-mode-listener @@ -112,7 +95,7 @@ var ext_entity_acl = function ($, connection, getEntityVersion, getEntityID, log init: init }; -}($, connection, getEntityVersion, getEntityID, log.getLogger("ext_entity_acl"), navbar, userIsAdministrator); +}($, connection, getEntityVersion, getEntityID, log.getLogger("ext_entity_acl")); $(document).ready(function () { if ("${BUILD_MODULE_EXT_ENTITY_ACL}" == "ENABLED") { diff --git a/src/core/js/ext_prop_display.js b/src/core/js/ext_prop_display.js index 3a8c616f503caadd4c0df04800bc8c15f0c068b2..bc4dc049b409fc1628704a6c778a2ec1c55ad638 100644 --- a/src/core/js/ext_prop_display.js +++ b/src/core/js/ext_prop_display.js @@ -21,9 +21,6 @@ 'use strict'; /** - * Hide or show properties for specific roles and users - * see src/doc/extension/display_of_properties.rst for documentation - * * @requires jQuery (library) * @requires log (singleton from loglevel library) * @requires load_config (function from webcaosdb.js) diff --git a/src/core/js/ext_trigger_crawler_form.js b/src/core/js/ext_trigger_crawler_form.js index a6e1e3a18a3582cc9b3e511880b72d15f730b346..0796ef77da36e730b05d70dbbd2e8728c6e65c79 100644 --- a/src/core/js/ext_trigger_crawler_form.js +++ b/src/core/js/ext_trigger_crawler_form.js @@ -41,7 +41,7 @@ * variable `BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX`. The default is * `Tools`. */ -var ext_trigger_crawler_form = function ($, form_elements) { +var ext_trigger_crawler_form = function () { var init = function (toolbox) { const _toolbox = toolbox || "${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX}"; @@ -52,7 +52,7 @@ var ext_trigger_crawler_form = function ($, form_elements) { const crawler_form = make_scripting_caller_form( script, button_name); - const modal = form_elements.make_form_modal(crawler_form, "Trigger the crawler", "Crawl the selected path"); + const modal = make_form_modal(crawler_form); navbar.add_tool(button_name, _toolbox, { @@ -63,6 +63,32 @@ var ext_trigger_crawler_form = function ($, form_elements) { }); } + /** + * Wrap the form into a Bootstrap modal. + */ + var make_form_modal = function (form) { + const title = "Trigger the Crawler"; + const modal = $(` + <div class="modal fade" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" + class="btn-close" + data-bs-dismiss="modal" + aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + <h4 class="modal-title">${title}</h4> + </div> + <div class="modal-body"> + </div> + </div> + </div>`); + modal.find(".modal-body").append(form); + return modal[0]; + } + /** * Create the trigger crawler form. */ @@ -78,7 +104,15 @@ var ext_trigger_crawler_form = function ($, form_elements) { }); $(warning_checkbox).find("input").attr("value", "TRUE"); - const scripting_caller = form_elements.make_scripting_submission_button(script, button_name); + const scripting_caller = $(` + <form method="POST" action="/scripting"> + <input type="hidden" name="call" value="${script}"/> + <input type="hidden" name="-p0" value=""/> + <div class="form-control"> + <input type="submit" + class="form-control btn btn-primary" value="${button_name}"/> + </div> + </form>`); scripting_caller.prepend(warning_checkbox).prepend(path_field); @@ -89,7 +123,7 @@ var ext_trigger_crawler_form = function ($, form_elements) { init: init, }; -}($, form_elements); +}(); $(document).ready(function () { if ("${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM}" === "ENABLED") { diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index 4b5bdf55e555a5755280b558770e4b2995731de7..9ae616daaf8ee79ab4b2a3600565ae540ae33f39 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -1437,10 +1437,6 @@ var form_elements = new function () { /** * Return a select field. * - * IMPORTANT: The select picker has to be initialized by the client by - * calling ``form_elements.init_select_picker(ret, config.value)`` (see - * below and https://gitlab.com/caosdb/caosdb-webui/-/issues/208). - * * @param {SelectFieldConfig} config * @returns {HTMLElement} a select field. */ @@ -1458,7 +1454,7 @@ var form_elements = new function () { // case when this method is called and is controlled by the client. So // there is currently no other work-around than to call // init_select_picker after the form creation explicitely :( - // form_elements.init_select_picker(ret, config.value); + //form_elements.init_select_picker(select[0], config.value); return ret; } @@ -1567,58 +1563,6 @@ var form_elements = new function () { } } - /** - * Return a modal HTML element containing the given form. - * - * @param {HTMLElement} form - the form to be shown in the modal - * @param {string} title - the title of the form modal - * @param {string} explanationText - An optional paragraph shown between - * modal title and form. - */ - this.make_form_modal = function (form, title, explanationText) { - const modal = $(` - <div class="modal fade" tabindex="-1" role="dialog"> - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h4 class="modal-title">${title}</h4> - <button type="button" - class="btn-close" - data-bs-dismiss="modal" - aria-label="Close"> - </button> - </div> - <div class="modal-body"> - <p>${explanationText}</p> - </div> - </div> - </div>`); - - modal.find(".modal-body").append(form); - return modal[0]; - } - - /** - * Return a submission button that triggers a given server-side-script. - * - * @param {string} script - Name of the server-side script to be triggered - * @param {string} buttonName - Display name of the submission button - */ - this.make_scripting_submission_button = function (script, buttonName) { - let button_name = buttonName || "Submit"; - const scripting_caller = $(` - <form method="POST" action="/scripting"> - <input type="hidden" name="call" value="${script}"/> - <input type="hidden" name="-p0" value=""/> - <div class="form-group"> - <input type="submit" - class="form-control btn btn-primary" value="${button_name}"/> - </div> - </form>`); - - return scripting_caller - } - /** * Return an input and a label, wrapped in a div with class diff --git a/src/core/js/form_panel.js b/src/core/js/form_panel.js index a745f949f98d6219e377783d7ca6c854908df573..9728a4ccea54c36d85399a3148373b7372108db0 100644 --- a/src/core/js/form_panel.js +++ b/src/core/js/form_panel.js @@ -65,33 +65,15 @@ var form_panel = new function () { }; /** - * Creates a callback function that creates the form inside - * the form panel when called for the first time. - * - * You may supply 'undefined' as form_config and use form_creator - * instead which is supposed to be a function without argument that - * return the form to be added to the panel + * Creates a callback function that toggles the form panel which */ - this.create_show_form_callback = function ( - panel_id, title, form_config, form_creator=undefined - ) { + this.create_show_form_callback = function (panel_id, title, form_config) { return (e) => { logger.trace("enter show_form_panel", e); const panel = $(form_panel.get_form_panel(panel_id, title)); if (panel.find("form").length === 0) { - if (form_config != undefined && form_creator!=undefined){ - throw new Error("create_show_form_callback takes either a FormConfig or a function that creates the form"); - } - - var form; - if (form_config != undefined ){ - form = form_elements.make_form(form_config); - } else if (form_creator != undefined ){ - form = form_creator(); - } else { - throw new Error("create_show_form_callback takes a FormConfig or a function that creates the form"); - } + const form = form_elements.make_form(form_config); panel.append(form); $(form).find(".selectpicker").selectpicker(); diff --git a/src/core/js/query_form.js b/src/core/js/query_form.js deleted file mode 100644 index 6775fee677660b32387dcd82346753eb98c94da0..0000000000000000000000000000000000000000 --- a/src/core/js/query_form.js +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Extend the functionality of the pure html query panel. - * - * Deprecated. This is to be replaced by the query form of caosdb-webui-core-components. - * - * @deprecated - * @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=" + encodeURIComponent(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 1d6d5c8326d3f7dc0f311404f825206c9303462f..1f7d9991633aedd554a79000ad99f17b06189d73 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -1200,6 +1200,242 @@ 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. @@ -1676,6 +1912,12 @@ 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%'); diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index 85c6f0b4944872ef270a8d64089af720514e1b06..8202ccea2a40d304943556612de40b00931f9648 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -387,9 +387,6 @@ <xsl:with-param name="boolean"> <xsl:value-of select="'false'"/> </xsl:with-param> - <xsl:with-param name="datetime"> - <xsl:value-of select="'false'"/> - </xsl:with-param> </xsl:call-template> </xsl:for-each> <xsl:for-each select="Record|RecordType|File|Property"> @@ -403,9 +400,6 @@ <xsl:with-param name="boolean"> <xsl:value-of select="'false'"/> </xsl:with-param> - <xsl:with-param name="datetime"> - <xsl:value-of select="'false'"/> - </xsl:with-param> </xsl:call-template> </xsl:for-each> </xsl:element> diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl index 1817df4056bc98f79c7a26a7540383ee8898e400..7c1d8bc897eb1e51344d54cfa81a62e4328b0467 100644 --- a/src/core/xsl/navbar.xsl +++ b/src/core/xsl/navbar.xsl @@ -51,7 +51,7 @@ <nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top mb-2 flex-column" id="caosdb-navbar-full"> <noscript>Please enable JavaScript!</noscript> <div class="container-fluid"> - <a class="navbar-brand d-lg-none d-inline"> + <a class="navbar-brand"> <xsl:attribute name="href"> <xsl:value-of select="$basepath"/> </xsl:attribute> @@ -69,20 +69,6 @@ <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="top-navbar"> - <a class="navbar-brand d-none d-lg-inline"> - <xsl:attribute name="href"> - <xsl:value-of select="$basepath"/> - </xsl:attribute> - <xsl:element name="img"> - <xsl:if test="'${BUILD_NAVBAR_BRAND_NAME}' != ''"> - <xsl:attribute name="class">caosdb-logo</xsl:attribute> - </xsl:if> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/${BUILD_NAVBAR_LOGO}')"/> - </xsl:attribute> - </xsl:element> - ${BUILD_NAVBAR_BRAND_NAME} - </a> <ul class="navbar-nav caosdb-navbar me-auto"> <li class="nav-item dropdown" id="caosdb-navbar-entities"> <a class="nav-link dropdown-toggle" role="button" id="navbarEntitiesMenuLink" data-bs-toggle="dropdown" aria-expanded="false" href="#"> diff --git a/test/core/js/modules/ext_cosmetics.js.js b/test/core/js/modules/ext_cosmetics.js.js index f51226ee01a1607c9ba1580c3760500fdec760b1..969c8297b8b5cf85d0a668d7c30e8b0f45e34d4d 100644 --- a/test/core/js/modules/ext_cosmetics.js.js +++ b/test/core/js/modules/ext_cosmetics.js.js @@ -1,9 +1,8 @@ /* * This file is a part of the CaosDB Project. * - * Copyright (C) 2021-2023 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> - * Copyright (C) 2023 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 @@ -37,30 +36,6 @@ QUnit.module("ext_cosmetics.js", { } }); -QUnit.test("custom datetime", function (assert) { - assert.ok(cosmetics.custom_datetime, "custom_datetime available"); - var test_cases = [ - ["1234-56-78", "1234-56-78"], - ["9876-54-32T12:34:56", "9876-54-32 12:34:56"], - ["2023-03-78T99:99:99+0800", "2023-03-78 99:99:99"], - ]; - - for (let test_case of test_cases) { - const container = $('<div></div>'); - $(document.body).append(container); - const text_value = $(`<span class="caosdb-f-property-datetime-value">${test_case[0]}</span>`); - container.append(text_value); - assert.equal($(container).find(" ").length, 0, "Test original datetime."); - cosmetics.custom_datetime(); - const newValueElement = - container[0].querySelector("span.caosdb-v-property-datetime-customized-newvalue"); - assert.ok(newValueElement, "Datetime customization: Test if result exists."); - assert.equal(newValueElement.innerHTML, test_case[1], - "Datetime customization: compared result."); - container.remove(); - } -}); - QUnit.test("linkify - https", function (assert) { assert.ok(cosmetics.linkify, "linkify available"); var test_cases = [ diff --git a/test/core/js/modules/form_panel.js.js b/test/core/js/modules/form_panel.js.js index a810fb8711f14bc3b3637297c9f937884022fc15..be4c4ad8e66402adedf8c386ff086be4d7cb7955 100644 --- a/test/core/js/modules/form_panel.js.js +++ b/test/core/js/modules/form_panel.js.js @@ -55,18 +55,9 @@ QUnit.test("create_show_form_callback ", function (assert) { help: help_text, }, ], }; - const cb = form_panel.create_show_form_callback(panel_id, title, csv_form_config); + const cb = form_panel.create_show_form_callback( panel_id, title, csv_form_config); assert.equal(typeof cb, "function", "function created"); cb() - - const creator = function() { - return form_elements.make_form(csv_form_config); - } - const cb2 = form_panel.create_show_form_callback( - panel_id, title, undefined, creator - ); - assert.equal(typeof cb2, "function", "function created"); - cb2() });