Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • caosdb/src/caosdb-webui
1 result
Show changes
Commits on Source (28)
Showing with 503 additions and 322 deletions
......@@ -26,6 +26,7 @@ guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md)
- [ ] All automated tests pass
- [ ] Reference related issues
- [ ] Up-to-date CHANGELOG.md (or not necessary)
- [ ] Up-to-date JSON schema (or not necessary)
- [ ] Appropriate user and developer documentation (or not necessary)
- How do I use the software? Assume "stupid" users.
- How do I develop or debug the software? Assume novice developers.
......
......@@ -4,25 +4,46 @@ 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.1] - 2023-02-14
## [Unreleased] - 2023-?
### Added
* `caosdb-v-property-linkified` css class to denote properties that have been
linkified already.
* `caosdb-f-property-datetime-value` css class for special handling of datetime properties. Also
there is now very basic customization (via variable `BUILD_MODULE_EXT_COSMETICS_CUSTOMDATETIME`)
for how datetime values can be displayed, which comes along with the new css classes
`caosdb-v-property-datetime-customized` and `caosdb-v-property-datetime-customized-newvalue`.
* `form_elements.make_form_modal` and
`form_elements.make_scripting_submission_button` functions to create a form
modal and an SSS submission button, respectively.
### Changed (for changes in existing functionality)
### Deprecated
* `query_form` module. Enable/disable via build property
`BUILD_MODULE_LEGACY_QUERY_FORM={ENABLED,DISABLED}`. To be removed when
caosdb-webui-core-components are included into this webui permanently.
### Removed
### Fixed
* [#194](https://gitlab.com/caosdb/caosdb-webui/-/issues/194) - Properties
remain hidden in previews of referenced entities
* [#199](https://gitlab.com/caosdb/caosdb-webui/-/issues/199) - Linkify creates
additional links after showing previews of referenced entities
### Security
### Documentation
## [0.10.1] - 2023-02-14
### Fixed
* [#194](https://gitlab.com/caosdb/caosdb-webui/-/issues/194) - Properties
remain hidden in previews of referenced entities
## [0.10.0] - 2022-12-19
(Florian Spreckelsen)
......
......@@ -52,8 +52,10 @@ 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=ENABLED
BUILD_MODULE_USER_MANAGEMENT=ENABLED
BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB
......@@ -168,6 +170,7 @@ MODULE_DEPENDENCIES=(
loglevel.js
plotly.js
webcaosdb.js
query_form.js
pako.js
utif.js
ext_version_history.js
......
/*
* This file is a part of the CaosDB Project.
*
* Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
* Copyright (C) 2021-2023 IndiScale GmbH <info@indiscale.com>
* Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
* Copyright (C) 2023 Florian Spreckelsen <f.spreckelsen@indiscale.com>
* 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
......@@ -30,14 +32,38 @@ var cosmetics = new function () {
/**
* Cut-off length of links. When linkify processes the links any href
* longer than this will be cut off at character 25 and "[...]" will be
* longer than this will be cut off at the end 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 (/https?:\/\//.test(this.innerText)) {
if (!($(this).hasClass("caosdb-v-property-linkified")) && (/https?:\/\//.test(this.innerText))) {
var result = this.innerText.replace(/https?:\/\/[^\s]*/g, function (href, index) {
var link_text = href;
if (_link_cut_off_length > 4 && link_text.length > _link_cut_off_length) {
......@@ -47,12 +73,31 @@ 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.
......@@ -69,6 +114,10 @@ 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();
......@@ -80,4 +129,4 @@ var cosmetics = new function () {
$(document).ready(function () {
caosdb_modules.register(cosmetics);
});
\ No newline at end of file
});
......@@ -21,6 +21,9 @@
'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)
......
......@@ -41,7 +41,7 @@
* variable `BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX`. The default is
* `Tools`.
*/
var ext_trigger_crawler_form = function () {
var ext_trigger_crawler_form = function ($, form_elements) {
var init = function (toolbox) {
const _toolbox = toolbox || "${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX}";
......@@ -52,7 +52,7 @@ var ext_trigger_crawler_form = function () {
const crawler_form = make_scripting_caller_form(
script, button_name);
const modal = make_form_modal(crawler_form);
const modal = form_elements.make_form_modal(crawler_form, "Trigger the crawler", "Crawl the selected path");
navbar.add_tool(button_name, _toolbox, {
......@@ -63,32 +63,6 @@ var ext_trigger_crawler_form = function () {
});
}
/**
* 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">&times;</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.
*/
......@@ -104,15 +78,7 @@ var ext_trigger_crawler_form = function () {
});
$(warning_checkbox).find("input").attr("value", "TRUE");
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>`);
const scripting_caller = form_elements.make_scripting_submission_button(script, button_name);
scripting_caller.prepend(warning_checkbox).prepend(path_field);
......@@ -123,7 +89,7 @@ var ext_trigger_crawler_form = function () {
init: init,
};
}();
}($, form_elements);
$(document).ready(function () {
if ("${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM}" === "ENABLED") {
......
......@@ -1437,6 +1437,10 @@ 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.
*/
......@@ -1454,7 +1458,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(select[0], config.value);
// form_elements.init_select_picker(ret, config.value);
return ret;
}
......@@ -1563,6 +1567,58 @@ 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
......
/**
* 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=" + 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);
}
}
});
......@@ -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%');
......
......@@ -297,6 +297,7 @@
<xsl:param name="value"/>
<xsl:param name="reference"/>
<xsl:param name="boolean"/>
<xsl:param name="datetime"/>
<xsl:choose>
<xsl:when test="normalize-space($value)!=''">
<xsl:choose>
......@@ -322,6 +323,14 @@
<xsl:value-of select="normalize-space($value)"/>
</xsl:element>
</xsl:when>
<xsl:when test="$datetime='true'">
<xsl:element name="span">
<xsl:attribute name="class">
<xsl:value-of select="'caosdb-f-property-single-raw-value caosdb-property-datetime-value caosdb-f-property-datetime-value caosdb-v-property-datetime-value'"/>
</xsl:attribute>
<xsl:value-of select="$value"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="span">
<xsl:attribute name="class">
......@@ -362,6 +371,9 @@
<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">
......@@ -375,6 +387,9 @@
<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>
......@@ -398,6 +413,9 @@
<xsl:with-param name="boolean">
<xsl:value-of select="../@datatype='LIST&lt;BOOLEAN>'"/>
</xsl:with-param>
<xsl:with-param name="datetime">
<xsl:value-of select="../@datatype='LIST&lt;DATETIME>'"/>
</xsl:with-param>
</xsl:call-template>
</xsl:element>
</xsl:for-each>
......@@ -449,6 +467,9 @@
<xsl:with-param name="boolean">
<xsl:value-of select="@datatype='BOOLEAN'"/>
</xsl:with-param>
<xsl:with-param name="datetime">
<xsl:value-of select="@datatype='DATETIME'"/>
</xsl:with-param>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
......
......@@ -26,9 +26,9 @@ copyright = '2022, IndiScale GmbH'
author = 'Daniel Hornung'
# The short X.Y version
version = '0.10.1'
version = '0.10.2'
# The full version, including alpha/beta/rc tags
release = '0.10.1-SNAPSHOT'
release = '0.10.2-SNAPSHOT'
# -- General configuration ---------------------------------------------------
......
......@@ -22,7 +22,7 @@ The following code snippet adds a form to the body of the HTML document.
const config = {
name: "my_form",
fields: [
{ type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
{ type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Experiment", required: true },
{ type: "integer", name: "number", label: "A Number", required: true },
{ type: "date", name: "date", label: "A Date", required: false },
{ type: "text", name: "comment", label: "A Comment", required: false },
......@@ -85,7 +85,7 @@ If you intend to call a server-side script, the config has to be changed a litte
const config = {
script: "process.py",
fields: [
{ type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
{ type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Experiment", required: true },
{ type: "integer", name: "number", label: "A Number", required: true },
{ type: "date", name: "date", label: "A Date", required: false },
{ type: "text", name: "comment", label: "A Comment", required: false },
......
# How to add a module to CaosDB WebUI
The CaosDB WebUI is organized in modules which can easily be added and on a module basis enabled or disabled.
The CaosDB WebUI is organized in modules which can easily be added and enabled or disabled per module.
There are a few steps necessary to create a new module.
Only a few steps are necessary to create a new module.
## Create the module file
Create a new file for each new module. We have the convention, that extensions
which are optional and should stay that way and also custom extensions for
special purposes to name the file starting with `ext_`. E.g.
`ext_flight_preview.js`.
Create a new file for each new module. We have the convention that extensions which are optional
(and also custom extensions for special purposes) are saved in files starting with `ext_`,
e.g. `ext_flight_preview.js`.
This file should define one function that wraps every thing and which is
This file should define one function that wraps everything, this function is then
enabled at the bottom of the file:
```js
......
===============
Query shortcuts
===============
Introduction
============
......@@ -7,11 +11,10 @@ data as query strings which are used frequently. They can be stored and
reused.
.. figure:: images/shortcut_toolbox.png
:alt: The Shortcuts in the Query Panel; Note the Toolbox for in the
top right
:alt: Shortcuts in the query panel. There is a
toolbox for editing shortcuts in the top right.
The Shortcuts in the Query Panel; Note the Toolbox for in the top
right
Shortcuts in the query panel. Note the toolbox for editing shortcuts in the top right
There are two ways to integrate query templates into the WebUI:
......@@ -47,7 +50,7 @@ It now opens a form with two input fields, ``Description`` and
The view to create a new shortcut
See `2.4 <#basic-shortcut>`__ and `2.5 <#advanced-shortcut>`__ for
See `Basic Shortcut`_ and `Advanced Shortcut`_ for
further explanation of the components of a Query Shortcut.
Edit the fields and click ``Submit`` for the creation of the new
......@@ -177,6 +180,10 @@ Each placeholder *id* must occur only once in both components – if you
need to use two years in your shortcut you have to use ``{year1}`` and
``{year2}`` or any other combinations of placeholder *ids*.
Global Shortcuts
================
Example for global_query_shortcuts.json
---------------------------------------
......@@ -191,7 +198,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}"
"query": "SELECT date, project, identifier FROM Experiment with date in {year}"
}
]
......
......@@ -14,31 +14,31 @@ the webinterface under the respective menu entry.
Let's start with a simple one::
FIND RECORD MusicalInstrument
FIND MusicalInstrument
Most queries simply start with the ``FIND`` keyword and describe what we are
looking for behind that. The ``RECORD`` keyword denotes that we are only looking
for Records (and not Files, Properties or RecordTypes). Finally, we provided
a RecordType name: MusicalInstrument. This means that we will get all Records
that have this RecordType as parent. Try it out!
looking for behind that. Then, we provided a RecordType name:
MusicalInstrument. This means that we will get all Records that have this
RecordType as parent. Try it out!
Let's look at::
FIND Guitar
FIND ENTITY Guitar
When we leave out the ``RECORD`` keyword, we will get every entity that is a
Guitar. When you submit this query you should find also a RecordType Guitar
in the results. Using ``FIND RecordType Guitar`` would restrict the result to
only that RecordType.
When we add the ``ENTITY`` keyword we will get every entity that is a
Guitar -- also the RecordType, and even a Property with that name if there
exists one. Using ``FIND RecordType Guitar`` would restrict the result to
only that RecordType. And ``FIND RECORD MusicalInstrument`` is just equivalent
to ``FIND MusicalInstrument``.
Note, that you cannot only provide RecordType names after the ``FIND``, but names
in general: ``FIND RECORD Nice Guitar``. This will give you a Record with the
Note, that you can provide not only RecordType names after the ``FIND``, but names
in general: ``FIND "Nice Guitar"``. This will give you a Record with the
name "Nice Guitar" (if one exists... and there should be one in the demo instance).
While it does not matter whether you use capital letters or not, the names have to
be exact. There are two features that make it easy to use names for querying
in spite of this:
- You can use "*" to match any string. E.g. ``FIND RECORD Nice*``
- You can use "*" to match any string. E.g. ``FIND Nice*``
- After typing three letters, names that start with those three are
suggested by the auto completion.
......@@ -64,7 +64,9 @@ result set. In general this looks like::
FIND <Name> <Property Filter>
Typically, the filter has the form ``<Property> <Operator> <Value>``,
for example ``length >= 0.7mm``.
for example ``length >= 0.7mm``. Instead of the ``<Name>`` you can also use one
of the entity roles, namely ``RECORD``, ``RECORDTYPE``, ``FILE``, ``PROPERY``,
or ``ENTITY``.
There are many filters available. You can check the specification for a comprehensive description of
those. Here, we will only look at the most common examples.
......@@ -72,7 +74,7 @@ those. Here, we will only look at the most common examples.
If you only want to assure that Records have a certain Property, without imposing
constrains on the value, you can use::
FIND RECORD MusicalInstrument WITH Manufacturer
FIND MusicalInstrument WITH Manufacturer
Similarly, to what we saw above when using incomplete names, you can use a "*"
......@@ -138,6 +140,6 @@ information in a table. A comma separated list of Property names can be provided
Or::
SELECT quality_factor, report, date FROM Analysis WHICH REFERENCES A Guitar WITH electric=TRUE
SELECT quality_factor, report, date FROM Analysis WHICH REFERENCES A Guitar WITH electric=TRUE
/*
* This file is a part of the CaosDB Project.
*
* Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
* Copyright (C) 2021-2023 IndiScale GmbH <info@indiscale.com>
* Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
* Copyright (C) 2023 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
......@@ -36,6 +37,30 @@ 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 = [
......