diff --git a/CHANGELOG.md b/CHANGELOG.md index f0f46cddce0ffcd2450c3ec8bcc5b6693b2161c5..5fd6d4fcbc6f22c94e953cf5d482e0d56f743dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,21 +4,78 @@ 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). -## [Unpublished] +## [Unreleased] -### Added (for new features, dependecies etc.) +### Added -### Changed (for changes in existing functionality) +* [#172](https://gitlab.com/caosdb/caosdb-webui/-/issues/172) - Map can handle + geo locations in list of references. -### Deprecated (for soon-to-be removed features) +### Changed -### Removed (for now removed features) +### Deprecated -### Fixed (for any bug fixes) +### Removed -### Security (in case of vulnerabilities) +### Fixed -### Documentation (for notable additions or changes of the documentation) +### Security + +### Documentation + +## [0.6.0] - 2022-05-03 +(Daniel Hornung) + +### Changed + +* Renamed the person reference resolver. + +### Fixed + +* [webui#170](https://gitlab.com/caosdb/caosdb-webui/-/issues/170) + Autocompletion for "IS REFERENCED BY" leads to query syntax error + +## [0.5.0] - 2022-03-25 +(Timm Fitschen) + +### Added + +* entity_acl module which adds a button to each entity which links to the + webui-acm module. Enable via BUILD_MODULE_EXT_ENTITY_ACL=ENABLED. +* A `#version_history` URI fragment which can be used to directly open the modal + with the full version history of the first entity on the page. +* `BUILD_MODULE_SHOW_ID_IN_LABEL` build variable with which the showing of + entity ids together with their names if it is enabled (disabled by default). +* Introduced `caosdb-f-form-required-marker` and `caosdb-f-form-required-label` + css classes for the markers of required fields in CaosDB form elements. +* The navbar has now an html id `caosdb-navbar-full` + +### Changed + +* Added `show` option to caosdb_map.MapConfig for showing the map initially and + storing the state accross page reloads. Defaults to `false`. This also bumps + the version of the map to 0.4.1 as it changes the behavior but the change is + backwards-compatible for the map config. Clients need to update their + version string in their config file, tho. + +### Deprecated + +### Removed + +* globla `setNameID` function (replaced by `_setDescriptionNameID` which should + only be used be the non-public XML-serialization functions.) + +### Fixed + +* Fixed saving of text properties that were changed in the Source-Editing mode + of the WYSIWYG editor. +* [#266](https://gitlab.indiscale.com/caosdb/src/caosdb-webui/-/issues/266) + Fixed an issue whereby missing property descriptions in the edit mode would + lead to wrongly detected entity updates in the server + +### Security + +### Documentation ## [0.4.2] - 2021-12-06 @@ -37,7 +94,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Optional WYSIWYG editor with markdown output for text properties. Controled by the `BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT` build variable which is set do `DISABLED` by default. - - Added button to version history panel that allows restoring old versions +* Added button to version history panel that allows restoring old versions ### Changed (for changes in existing functionality) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index f3e1d1706dd8dd9bdbdab16377c2c75ed0299f7f..14b2adf30ad23bb7eaa224083c6bbb9c7f4f8a0f 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -1,4 +1,4 @@ -* CaosDB Server 0.5.x +* CaosDB Server 0.7.2 * Make 4.2.0 # Java Script Libraries (included in this repository) @@ -26,8 +26,8 @@ ## For CKEditor (WYSIWYG editor in edit mode) -* we're using a custom-built ckeditor 31.0.0 from - https://ckeditor.com/ckeditor-5/online-builder/ with a customized set of +* We're using a custom-built ckeditor 32.0.0 from [IndiScale's fork of CKEditor + 5](https://gitlab.indiscale.com/caosdb/src/ckeditor5) with a customized set of editor plugins. Please refer to the `package.json` within `libs/ckeditor...zip` for a full list of said plugins. diff --git a/Makefile b/Makefile index 5cca686319c617006007e4884cfb269796ac7af4..c209b5410f6e8111cac132ee55d4bebbb1a7426e 100644 --- a/Makefile +++ b/Makefile @@ -306,7 +306,7 @@ $(LIBS_DIR)/js/qrcode.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/qrcode-1.4.4/qrcode.min.js $@ $(LIBS_DIR)/js/ckeditor.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/ckeditor5-31.0.0-k356w86hp13l/build/ckeditor.js $@ + ln -s $(LIBS_DIR)/ckeditor5-build-custom/build/ckeditor.js $@ $(addprefix $(LIBS_DIR)/, js css): diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md index 9dd648c235258b052332212bd906525e87863629..f3dc40b40e9022907e5dbeab20b5c4bb7395c48e 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -18,12 +18,16 @@ guidelines of the CaosDB Project 2. Check all general prerequisites. -3. Merge the release branch into the main branch. +3. Update `src/doc/conf.py` version -4. Tag the latest commit of the main branch with `v<VERSION>`. +4. Merge the release branch into the main branch. -5. Delete the release branch. +5. Tag the latest commit of the main branch with `v<VERSION>`. -6. Merge the main branch back into the dev branch. +6. Delete the release branch. -7. Prepare CHANGELOG for next release cycle. +7. Merge the main branch back into the dev branch. + +8. Prepare for next release cycle: + * `CHANGELOG.md`: "Unreleased" section + * `src/doc/conf.py`: Bump to next version number and `x.y.z-SNAPSHOT` for the `release` variable. diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 535a6c846a5c39c48937dee43f087501f43285ae..cd57b632e2ea2b9ab005c7185a309a9594f123ff 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -53,12 +53,13 @@ BUILD_MODULE_EXT_ADD_QUERY_TO_BOOKMARKS=DISABLED 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_USER_MANAGEMENT=ENABLED BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED -BUILD_EXT_REFERENCES_CUSTOM_RESOLVER=person_reference +BUILD_EXT_REFERENCES_CUSTOM_RESOLVER=caosdb_default_person_reference BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT=DISABLED @@ -177,4 +178,5 @@ MODULE_DEPENDENCIES=( form_panel.js ckeditor.js ext_editmode_wysiwyg_text.js + reference_resolver/caosdb_default_person.js ) diff --git a/libs/ckeditor5-31.0.0-k356w86hp13l.zip b/libs/ckeditor5-31.0.0-k356w86hp13l.zip deleted file mode 100644 index 71523482a2bdfa7c0a9776eeed7aa7be010e053b..0000000000000000000000000000000000000000 Binary files a/libs/ckeditor5-31.0.0-k356w86hp13l.zip and /dev/null differ diff --git a/libs/ckeditor5-build-custom.zip b/libs/ckeditor5-build-custom.zip new file mode 100644 index 0000000000000000000000000000000000000000..cb06441337f97369c1bc83e2b627bafa7b584b6f Binary files /dev/null and b/libs/ckeditor5-build-custom.zip differ diff --git a/misc/map_test_data.py b/misc/map_test_data.py index addc0e8c7f52cc60d228e079e5b76f5893be74d4..eda56c0ef06fc73b42d062e4fe7536aaf49b8286 100755 --- a/misc/map_test_data.py +++ b/misc/map_test_data.py @@ -18,6 +18,9 @@ datamodel.extend([ "MapObject" ).add_property("longitude", importance=caosdb.OBLIGATORY ).add_property("latitude", importance=caosdb.OBLIGATORY), + caosdb.RecordType( + "PathObject" + ).add_property("MapObject", datatype=caosdb.LIST("MapObject")), ]) datamodel.insert() @@ -25,19 +28,23 @@ datamodel.insert() # test data +testdata = caosdb.Container() +path = caosdb.Record() +path.add_parent("PathObject") +path.add_property("MapObject", datatype=caosdb.LIST("MapObject"), value=[]) +testdata.append(path) +for i in range(100): + loc = caosdb.Record( + "Object-{}".format(i) + ).add_parent("MapObject" + ).add_property("longitude", random.gauss(-42.0, 5) + ).add_property("latitude", random.gauss(77.0, 5)) + testdata.append(loc) + path.get_property("MapObject").value.append(loc) -testdata = caosdb.Container() -for i in range(100): - testdata.append( - caosdb.Record( - "Object-{}".format(i) - ).add_parent("MapObject" - ).add_property("longitude", random.gauss(-42.0, 5) - ).add_property("latitude", random.gauss(77.0, 5)) - ) testdata.insert(); diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index b76a5fe9ecc9f6d866dc159429ef7401ac3ddd5d..e8b7b42086dd41b0c599fb93b5ac0c1977abfd06 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -279,9 +279,17 @@ h5 { left: 0px; } +.caosdb-label-link { + text-decoration: none; +} + +.caosdb-label-id { + margin-right: 0.3em; + display: none; +} + .caosdb-label-name { font-weight: bold; - text-decoration: none; } /* lists of values */ @@ -771,3 +779,15 @@ details p { font-size: 0.875rem; color: #5E6762; } + +.caosdb-f-form-wrapper .caosdb-f-form-required-marker { + font-size: 10px; + color: red; + margin-right: 4px; + font-weight: 100; +} + +.caosdb-f-form-elements-footer .caosdb-f-form-required-label { + margin-right: 4px; + font-size: 11px; +} diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js index fe8f0cba5e5795a23615650bf5516c344e012d54..39109d444ff4bc8bbd0f2b2811004967565a3361 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -5,8 +5,8 @@ * Copyright (C) 2018-2020 Alexander Schlemmer * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2019-2020 IndiScale GmbH (info@indiscale.com) - * Copyright (C) 2019-2020 Timm Fitschen (t.fitschen@indiscale.com) + * Copyright (C) 2019-2022 IndiScale GmbH (info@indiscale.com) + * Copyright (C) 2019-2022 Timm Fitschen (t.fitschen@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 @@ -604,6 +604,7 @@ function getPropertyFromElement(propertyelement, names = undefined) { if (valel && valel.textContent.length > 0) { value_string = valel.textContent; } else if (valel && valel.value && valel.value.length > 0) { + // this is the case when the valel is an input coming from edit_mode. value_string = valel.value; } @@ -627,20 +628,11 @@ function getPropertyFromElement(propertyelement, names = undefined) { if (property.list) { // list datatypes let listel; - if (property.reference) { - // list of referernces - listel = findElementByConditions(valel, x => x.classList.contains("caosdb-f-reference-value"), - x => x.classList.contains("caosdb-preview-container")); - for (var j = 0; j < listel.length; j++) { - property.value.push(getIDfromHREF(listel[j])); - } - } else { - // list of anything but references - listel = findElementByConditions(valel, x => x.classList.contains("list-inline-item"), - x => x.classList.contains("caosdb-preview-container")); - for (var j = 0; j < listel.length; j++) { - property.value.push(listel[j].textContent); - } + // list of anything but references + listel = findElementByConditions(valel, x => x.classList.contains("caosdb-f-property-single-raw-value"), + x => x.classList.contains("caosdb-preview-container")); + for (var j = 0; j < listel.length; j++) { + property.value.push(listel[j].textContent); } } else if (property.reference && valel.getElementsByTagName("a")[0]) { // reference datatypes @@ -651,7 +643,6 @@ function getPropertyFromElement(propertyelement, names = undefined) { } } - return property; } @@ -743,6 +734,16 @@ var _constructXpaths = function (selectors) { * * `getPropertyValues(entities, [["Geo Location", "latitude"], ["Geo Location", "longitude"]])` * + * When the entitieshave normal non-list references to the "Geo Location" the + * result looks like this: + * + * `[[ "50", "-39"], ...]` + * + * When the entities have a LIST of thre Geo Locations the result looks like + * this: + * + * `[[[ "50", "51", "52"], [ "-39", "-38", "-37" ]], ...]`. + * * Use empty strings for selector elements when the property name is irrelevant: * * `getPropertyValues(entities, [["", "latitude"], ["", "longitude"]])` @@ -757,11 +758,14 @@ var _constructXpaths = function (selectors) { * special cases ("name", "description", "unit", etc.) are to be added when * needed. * - * @param {XMLElement[]) entities + * @param {XMLElement[]} entities * @param {String[][]} selectors - * @return {String[][]} A table of the property values for each entity. + * @return {String[][]} A table of the property values for each entity (index + * order is `[row][column]`). Each row is an entity, each column is a value + * (or an array of values, when the entity has list properties). */ var getPropertyValues = function (entities, selectors) { + // @review Florian Spreckelsen 2022-05-06 const entity_iter = entities.evaluate("/Response/Record", entities); const table = []; @@ -771,9 +775,20 @@ var getPropertyValues = function (entities, selectors) { while (current_entity) { const row = []; for (let expr of xpaths) { - const property = entities.evaluate(expr, current_entity).iterateNext(); - if (typeof property != "undefined" && property != null) { - row.push(property.textContent.trim()); + const property_iter = entities.evaluate(expr, current_entity); + var property = property_iter.iterateNext(); + if (typeof property !== "undefined" && property !== null) { + // handle lists and single values + var values = []; + while (property !== null) { + values.push(property.textContent.trim()); + property = property_iter.iterateNext(); + } + if(values.length < 2) { + // wasn't a list + values = values[0]; + } + row.push(values); } else { row.push(undefined) } diff --git a/src/core/js/ext_autocomplete.js b/src/core/js/ext_autocomplete.js index b070568e0e627955149d9a08bcdcbf5be0c1b6ec..932a9466a83f17594894ad389f6535bcd6caffa2 100644 --- a/src/core/js/ext_autocomplete.js +++ b/src/core/js/ext_autocomplete.js @@ -159,7 +159,7 @@ var ext_autocomplete = new function () { var end = origJQElement[0].value.slice(cursorpos); var result = resultsFromServer.map(x => { var x_quoted = x; - if(!(ext_autocomplete.CQL_WORDS.indexOf(x)>-1) && x.indexOf(" ") > -1) { + if (ext_autocomplete.CQL_WORDS.indexOf(x) == -1 && x.indexOf(" ") > -1) { if(x.indexOf("\"") > -1) { x_quoted = `'${x}'`; } else { diff --git a/src/core/js/ext_editmode_wysiwyg_text.js b/src/core/js/ext_editmode_wysiwyg_text.js index f784dbd5d998ffeb95b4d594c907ff725441eadd..d5cd88a85084c76fd92c8710d9845bd80f09fb95 100644 --- a/src/core/js/ext_editmode_wysiwyg_text.js +++ b/src/core/js/ext_editmode_wysiwyg_text.js @@ -39,6 +39,8 @@ */ var ext_editmode_wysiwyg_text = function ($, logger, ClassicEditor, edit_mode, getPropertyElements, getPropertyDatatype, getPropertyName) { + var _callOnSave = []; + var insertEditorInProperty = async function (prop) { if (!(getPropertyDatatype(prop) === 'TEXT')) { // Ignore anything that isn't a list property, even LIST<TEXT> @@ -62,7 +64,7 @@ var ext_editmode_wysiwyg_text = function ($, logger, ClassicEditor, edit_mode, g logger.debug('Initialized editor for ' + getPropertyName(prop)); // Manually implement saving the data since edit mode is not // a form to be submitted. - editor.model.document.on("change:data", (e) => { + _callOnSave.push(() => { editor.updateSourceElement(); }); } catch (error) { @@ -70,7 +72,23 @@ var ext_editmode_wysiwyg_text = function ($, logger, ClassicEditor, edit_mode, g } } + const proxySaveMethod = function (original) { + const result = function (entity) { + _callOnSave.forEach(cb => { + cb(); + }); + if (typeof original === "function") { + return original(entity); + } + return undefined; + } + return result; + } + var replaceTextAreas = function (entity) { + // on save, call callbacks + edit_mode.app.onBeforeInsert = proxySaveMethod(edit_mode.app.onBeforeInsert); + edit_mode.app.onBeforeUpdate = proxySaveMethod(edit_mode.app.onBeforeUpdate); const properties = getPropertyElements(entity); for (let prop of properties) { // TODO(fspreck): This will be replaced by a whitelist of properties @@ -107,6 +125,12 @@ var ext_editmode_wysiwyg_text = function ($, logger, ClassicEditor, edit_mode, g logger.debug('Re-rendering ' + getPropertyName(e.target)); ext_editmode_wysiwyg_text.insertEditorInProperty(e.target); }, true); + + // Clear list of saving callbacks when leaving the edit mode (regardless + // of saving or cancelling) + document.body.addEventListener(edit_mode.end_edit.type, (e) => { + this._callOnSave = []; + }, true); }; return { diff --git a/src/core/js/ext_entity_acl.js b/src/core/js/ext_entity_acl.js new file mode 100644 index 0000000000000000000000000000000000000000..ce6d4160afefffa11a346804a565f832a0aca84e --- /dev/null +++ b/src/core/js/ext_entity_acl.js @@ -0,0 +1,104 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +"use strict"; + +/** + * Adds button to each entity which links to the entity's ACL. + * + * Please enable via build property: + * + * BUILD_MODULE_EXT_ENTITY_ACL=ENABLED + * + * Set the base uri of the EntityACL view (optional, defaults to + * `${connection.getBasePath()}webinterface/acm/entityacl/`): + * + * BUILD_MODULE_EXT_ENTITY_ACL_URI_ROOT=[scheme://host:port]/what/evs + * + * + * @author Timm Fitschen + */ +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"; + const _entity_acl_link_class = "caosdb-f-entity-entity_acl-button"; + const _entity_acl_canvas_container = "caosdb-f-entity-entity_acl"; + const _entity_acl_link_container = "caosdb-f-entity-entity_acl-link"; + const _entity_acl_icon = `<i class="bi bi-key"></i>`; + + /** + * Create a link to the Entity ACL. + * + * @param {string} entity_id + * @return {HTMLElement} the newly created link. + */ + var create_entity_acl_link = function (entity_id) { + const button = $(`<a href="${BUILD_MODULE_EXT_ENTITY_ACL_URI_ROOT}${entity_id}" title="Open Entity ACL" class="${_entity_acl_link_class} caosdb-v-entity-entity_acl-button btn">${_entity_acl_icon}</a>`); + return button[0]; + } + + /** + * Add a entity_acl button to a given entity. + * @param {HTMLElement} entity + */ + var add_entity_acl_to_entity = function (entity) { + const entity_id = getEntityID(entity); + + $(entity).find(`.${_buttons_list_class}`).append(create_entity_acl_link(entity_id)); + } + + var remove_entity_acl_link = function (entity) { + $(entity).find(`.${_buttons_list_class} .${_entity_acl_link_class}`).remove(); + } + + var _init = function () { + for (let entity of $(".caosdb-entity-panel")) { + remove_entity_acl_link(entity); + add_entity_acl_to_entity(entity); + } + } + + /** + * Initialize this module and append a QR Code button to all entities panels on the page. + * + * Removes all respective buttons if present before adding a new one. + */ + var init = function () { + _init(); + + // edit-mode-listener + document.body.addEventListener(edit_mode.end_edit.type, _init, true); + }; + + return { + add_entity_acl_to_entity: add_entity_acl_to_entity, + remove_entity_acl_link: remove_entity_acl_link, + create_entity_acl_link: create_entity_acl_link, + init: init + }; + +}($, connection, getEntityVersion, getEntityID, log.getLogger("ext_entity_acl")); + +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_ENTITY_ACL}" == "ENABLED") { + caosdb_modules.register(ext_entity_acl); + } +}); diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js index c20cdefc2e9de73a21db81e3a7d5ebacebe73416..39e6d510f9bbdf8e18426dc9b56577a235850a8d 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -25,7 +25,7 @@ /** * @module caosdb_map - * @version 0.4 + * @version 0.4.1 * * For displaying a geographical map which shows entities at their associated * geolocation. @@ -34,7 +34,7 @@ * `conf/ext/json/ext_map.json` and comply with the {@link MapConfig} type * which is described below. * - * The current version is 0.4. It is not considered to be stable because + * The current version is 0.4.1. It is not considered to be stable because * implementation of the graticule still is not satisfactory. * * Apart from that the specification of the configuration and the @@ -43,7 +43,7 @@ var caosdb_map = new function () { var logger = log.getLogger("caosdb_map"); - this.version = "0.4"; + this.version = "0.4.1"; this.dependencies = ["log", { "L": ["latlngGraticule", "Proj"] }, "navbar", "caosdb_utils"]; @@ -63,6 +63,9 @@ var caosdb_map = new function () { * integrated query generator. * * @typedef {object} MapConfig + * @property {boolean} [show=false] - show the map immediately when it is + * ready. This configuration option is being stored locally for keeping + * the map shown/hidden accross page reloads. * @property {string} version - the version of the map * @property {string} default_view - the view which is shown when the user * opens the map and has no stored view from prior visits to the map. @@ -336,6 +339,7 @@ var caosdb_map = new function () { */ this._default_config = { "version": this.version, + "show": false, "datamodel": { "lat": "latitude", "lng": "longitude", @@ -372,6 +376,8 @@ var caosdb_map = new function () { * @callback {mapEntityPopupGenerator} * @param {HTMLElement} entity - in HTML representation * @param {DataModelConfig} datamodel + * @param {Number} lat - latitude + * @param {Number} lng - longitude * @return {HTMLElement} a popup element. */ @@ -396,7 +402,7 @@ var caosdb_map = new function () { this._get_with_POV = function (props) { var pov = "" for (let p of props) { - pov = pov + ` WITH ${p} `; + pov = pov + ` WITH "${p}" `; } return pov; } @@ -440,12 +446,12 @@ var caosdb_map = new function () { var pov = undefined; if (typeof ids === "undefined") { pov = (caosdb_map._get_with_POV(props) + - ` WITH ${datamodel.lat} AND ${datamodel.lng}`); + ` WITH ( "${datamodel.lat}" AND "${datamodel.lng}" )`); } else { pov = caosdb_map._get_id_POV(ids); } - return `SELECT parent,${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY ${recordtype} ${pov} `; + return `SELECT parent,${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY "${recordtype}" ${pov} `; } @@ -537,12 +543,25 @@ var caosdb_map = new function () { * recordtype) */ this._set_subprops_at_top = function (entities, depth, datamodel) { + // @review Florian Spreckelsen 2022-05-06 var latlong = caosdb_map._get_leaf_prop(entities, depth, datamodel); for (let rec_id in latlong) { + const is_list_lat = Array.isArray(latlong[rec_id][0]); + const is_list_lng = Array.isArray(latlong[rec_id][0]); + var lat, lng; + if (is_list_lat) { + var lat_val = `<Value>${latlong[rec_id][0].join("</Value><Value>")}</Value>`; + lat = `<Property name="${datamodel.lat}" datatype="LIST<lat>">${lat_val}</Property>`; + var lng_val = `<Value>${latlong[rec_id][1].join("</Value><Value>")}</Value>`; + lng = `<Property name="${datamodel.lng}" datatype="LIST<lng>">${lng_val}</Property>`; + } else { + lat = `<Property name="${datamodel.lat}">${latlong[rec_id][0]}</Property>`; + lng = `<Property name="${datamodel.lng}">${latlong[rec_id][1]}</Property>`; + } let tmp_rec = caosdb_map._get_toplvl_rec_with_id(entities, rec_id); - tmp_rec.append(str2xml(`<Property name="${datamodel.lat}">${latlong[rec_id][0]}</Property>`).firstElementChild); - tmp_rec.append(str2xml(`<Property name="${datamodel.lng}">${latlong[rec_id][1]}</Property>`).firstElementChild); + tmp_rec.append(str2xml(lat).firstElementChild); + tmp_rec.append(str2xml(lng).firstElementChild); } } @@ -565,7 +584,7 @@ var caosdb_map = new function () { results = await transformation.transformEntities(entities); } else { results = await caosdb_map.query( - `FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`); + `FIND ENTITY WITH ( "${datamodel.lat}" AND "${datamodel.lng}" )`); } const container = $('<div>').append(results)[0]; @@ -582,11 +601,12 @@ var caosdb_map = new function () { * @param {HTMLElement} entity - an entity in HTML representation. * @param {DataModelConfig} datamodel - configuration of the properties * used for the coordinates. + * @param {Number} lat - latitude + * @param {Number} lng - longitude * @returns {HTMLElement} a popup element. */ - this._make_map_popup = function (entity, datamodel) { - const lat = getProperty(entity, datamodel.lat); - const lng = getProperty(entity, datamodel.lng); + this._make_map_popup = function (entity, datamodel, lat, lng) { + // @review Florian Spreckelsen 2022-05-06 const role_label = $(entity).find( ".label.caosdb-f-entity-role").first().clone(); const parent_list = caosdb_map.make_parent_labels(entity); @@ -599,8 +619,7 @@ var caosdb_map = new function () { extra_loc_hint = `<div>Location of related ${path[path.length-1]}<div>`; } const loc = $(`<div class="small text-muted">${extra_loc_hint} - Lat: ${dms_lat} Lng: ${dms_lng} - </div>`); + Lat: ${dms_lat} Lng: ${dms_lng}</div>`); const ret = $('<div/>') .append(role_label) .append(parent_list) @@ -843,8 +862,10 @@ var caosdb_map = new function () { * * The map panel is the HTMLElement which contains the map. Is is * hidden or shown when the user clicks on the toggle map button. + * + * @param {boolean} show - show this panel immediately. */ - this.init_map_panel = function () { + this.init_map_panel = function (show) { logger.trace("enter init_map_panel"); // remove old @@ -852,7 +873,9 @@ var caosdb_map = new function () { let panel = this.create_map_panel(); - $(panel).hide(); + if (!show) { + $(panel).hide(); + } $('nav').first().after(panel); logger.trace("leave init_map_panel"); @@ -865,6 +888,7 @@ var caosdb_map = new function () { */ this.toggle_map = function () { logger.trace("enter toggle_map"); + sessionStorage["caosdb_map.show"] = !$(".caosdb-f-map-panel").is(":visible"); $(".caosdb-f-map-panel").toggle(900, () => this ._toggle_cb()); } @@ -872,6 +896,7 @@ var caosdb_map = new function () { this.show_map = function () { logger.trace("enter show_map"); + sessionStorage["caosdb_map.show"] = "true"; $(".caosdb-f-map-panel").show(900, () => this ._toggle_cb()); } @@ -983,14 +1008,18 @@ var caosdb_map = new function () { return; } this.config = config; + var show_map = config.show; + if (sessionStorage["caosdb_map.show"]) { + show_map = JSON.parse(sessionStorage["caosdb_map.show"]); + } this.init_select_handler(); this.init_view_change_handler(); - panel = this.init_map_panel(); + panel = this.init_map_panel(show_map); // TODO split in smaller pieces and move callback to separate function this.change_map_view = (view) => { if (this._map) { - this._map._container.remove(); + this._map._container.remove(); this._map.remove(); } @@ -1081,8 +1110,8 @@ var caosdb_map = new function () { // indicate that the map is ready: map button is present and // map is hidden or shown but initialized in either case. - this._map.whenReady(()=>{ - document.body.dispatchEvent(caosdb_map.map_ready); + this._map.whenReady(() => { + document.body.dispatchEvent(caosdb_map.map_ready); }); } catch (err) { logger.error("Could not initialize the map.", @@ -1365,6 +1394,11 @@ var caosdb_map = new function () { ); result.views = this._unconfigured_views; } + try { + this.check_config(result); + } catch (error) { + logger.error(error.message); + } logger.trace("leave load_config", result); return result; } @@ -1499,6 +1533,34 @@ var caosdb_map = new function () { */ this.query = query; + /* + * Create a single map marker for the given entity. + * + * @param {HTMLElement} map_entity - the entity. + * @param {DataModelConfig} datamodel - specifies the properties for + * coordinates. + * @param {number} lat - latitude + * @param {number} lng - longitude + * @param {DivIcon_options} icon_options + * @param {number} zIndexOffset - zIndexOffset of the marker. + * @param {mapEntityPopupGenerator} [make_popup] - creates popup content. + * @returns {L.Marker} the marker for the map. + */ + var _create_single_entity_marker = function (map_entity, datamodel, lat, + lng, icon_options, zIndexOffset, make_popup) { + // @review Florian Spreckelsen 2022-05-06 + var marker = L.marker([lat, lng], { + icon: L.divIcon(icon_options) + }); + + if (zIndexOffset) { + marker.setZIndexOffset(zIndexOffset); + } + if (make_popup) { + marker.bindPopup(make_popup(map_entity, datamodel, lat, lng)); + } + return marker; + } /** * Create markers for the map for an array of entities. @@ -1512,32 +1574,82 @@ var caosdb_map = new function () { * @param {DivIcon_options} icon_options * @returns {L.Marker[]} an array of markers for the map. */ - this.create_entity_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) { - logger.trace("enter create_entity_markers", entities, datamodel, zIndexOffset, icon_options); + this.create_entity_markers = function (entities, datamodel, make_popup, + zIndexOffset, icon_options) { + // @review Florian Spreckelsen 2022-05-06 + logger.trace("enter create_entity_markers", entities, datamodel, + zIndexOffset, icon_options); var ret = [] for (const map_entity of entities) { - var lat = getProperty(map_entity, datamodel.lat); - var lng = getProperty(map_entity, datamodel.lng); + var lat_vals = getProperty(map_entity, datamodel.lat); + var lng_vals = getProperty(map_entity, datamodel.lng); - if (lat && lng) { - logger.debug(`create entity marker at [${lat}, ${lng}] for`, + if (!lng_vals || !lng_vals) { + logger.debug("undefined latitude or longitude", + map_entity, lat_vals, lng_vals); + continue; + } + + // we need lat_vals and lng_lavs to be arrays so we make them + // be one + var is_list_lat = true; + if (!Array.isArray(lat_vals)) { + lat_vals = [lat_vals]; + is_list_lat = false; + } + var is_list_lng = true; + if (!Array.isArray(lng_vals)) { + lng_vals = [lng_vals]; + is_list_lng = false; + } + + // both array's length must match + if (is_list_lng !== is_list_lat || + (is_list_lat && is_list_lng && + lat_vals.length !== lng_vals.length)) { + logger.error("Cannot show this entity on the map. " + + "Its lat/long properties have different lenghts: ", map_entity); - var marker = L.marker([lat, lng], { - icon: L.divIcon(icon_options) - }); + continue; + } - if (zIndexOffset) { - marker.setZIndexOffset(zIndexOffset); - } - if (make_popup) { - marker.bindPopup(make_popup(map_entity, datamodel)); - } + + // zip both arrays + // [lat1, lat2, ... latN] + // [lng1, lng2, ... lngN] + // into one + // [[lat1,lng1],[lat2,lng2],... [latN,lngN]] + var latlngs = lat_vals.map(function (e, i) { + return [e, lng_vals[i]]; + }); + logger.debug(`create point marker(s) at ${latlngs} for`, + map_entity); + for (let latlng of latlngs) { + var marker = _create_single_entity_marker(map_entity, + datamodel, latlng[0], latlng[1], + icon_options, zIndexOffset, make_popup); ret.push(marker); - } else { - logger.debug("undefined latitude or longitude", - map_entity, lat, lng); } + + /* Code for showing a PATH on the map. + * Maybe we re-use it later + * + logger.debug(`create path line at ${latlngs} for`, + map_entity); + + var opts = {color:'red', smoothFactor: 10.0, weight: 1.5, opacity: 0.5}; + var opts_2 = {color:'green', smoothFactor: 10.0, weight: 3, opacity: 0.5}; + var path = L.polyline(latlngs, opts); + if (make_popup) { + path.bindPopup(make_popup(map_entity, datamodel, lat, lng)); + } + path.on("mouseover",()=>path.setStyle(opts_2)); + path.on("mouseout",()=>path.setStyle(opts)); + ret.push(path); + * + * + */ } return ret; } diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index fe4d618c752490400e501116470cce0f28a909ad..cabf5741c2aab2ac7a9ca2d0d4c363f8a3530341 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -311,8 +311,8 @@ var resolve_references = new function () { * resolved as a string and returns a `reference_info` object with * the resolved custom reference as a `text` property. * - * See caosdb-webui/src/ext/js/person_reference_resolver.js for an - * example. + * See caosdb-webui/src/core/js/reference_resolver/caosdb_default_person.js + * for an example. * * TODO refactor to be configurable. @async @param {string} id - the id of * the entity which is to be resolved. @return {reference_info} diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index 193235a2f8a799c07ccc893742d5df9a7d0fa7d1..6815acd791213c6b239a693c3c64667965c369ed 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -1232,7 +1232,7 @@ var form_elements = new function () { .css({ "margin": "20px", }).append(this.make_required_marker()) - .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0]; + .append('<span class="caosdb-f-form-required-label">required field</span>')[0]; } this.make_error_message = function (message) { @@ -1502,14 +1502,7 @@ var form_elements = new function () { * @returns {HTMLElement} span element. */ this.make_required_marker = function () { - // TODO create class and move to css file - return $('<span>*</span>') - .css({ - "font-size": "10px", - "color": "red", - "margin-right": "4px", - "font-weight": "100", - })[0]; + return $('<span class="caosdb-f-form-required-marker">*</span>')[0]; } diff --git a/src/ext/js/person_reference_resover.js b/src/core/js/reference_resolver/caosdb_default_person.js similarity index 63% rename from src/ext/js/person_reference_resover.js rename to src/core/js/reference_resolver/caosdb_default_person.js index 393557354904787f04472585bca0883d64200d86..24f098c81d1c3f2c1f6dac6e9f6fe7d5b72f5667 100644 --- a/src/ext/js/person_reference_resover.js +++ b/src/core/js/reference_resolver/caosdb_default_person.js @@ -22,7 +22,7 @@ */ /** - * @module person_reference + * @module caosdb_default_person_reference * * Replace the reference to a Person Record by the values of that * Record's firstname and lastname properties. @@ -30,9 +30,9 @@ * TODO: Make name(s) of person RecordType(s) and names of firstname * and lastname properties configurable. */ -var person_reference = new function () { +var caosdb_default_person_reference = new function () { - var logger = log.getLogger("person_reference"); + var logger = log.getLogger("caosdb_default_person_reference"); const lastname_prop_name = "lastname" const firstname_prop_name = "firstname" @@ -42,24 +42,26 @@ var person_reference = new function () { * Return the name of a person as firstname + lastname */ this.get_person_str = function (el) { - var valpr = getProperties(el); - if (valpr == undefined) { - return; - } - return valpr.filter(valprel => - valprel.name.toLowerCase().trim() == - firstname_prop_name.toLowerCase())[0].value + - " " + - valpr.filter(valprel => valprel.name.toLowerCase().trim() == - lastname_prop_name.toLowerCase())[0].value; + var valpr = getProperties(el); + if (valpr == undefined) { + return; + } + return valpr.filter(valprel => + valprel.name.toLowerCase().trim() == + firstname_prop_name.toLowerCase())[0].value + + " " + + valpr.filter(valprel => valprel.name.toLowerCase().trim() == + lastname_prop_name.toLowerCase())[0].value; } this.resolve = async function (id) { - const entity = (await resolve_references.retrieve(id))[0]; + const entity = (await resolve_references.retrieve(id))[0]; - if (resolve_references.is_child(entity, person_rt_name)) { - return {"text": person_reference.get_person_str(entity)}; - } + if (resolve_references.is_child(entity, person_rt_name)) { + return { + "text": caosdb_default_person_reference.get_person_str(entity) + }; + } } } diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index 03cc7bfbb399e898a88cb99dc0a05cc90289f96a..6d8971c044af8231dddb6c9d8f9b823262120ca9 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -4,8 +4,10 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2019 IndiScale GmbH (info@indiscale.com) - * Copyright (C) 2019 Timm Fitschen (t.fitschen@indiscale.com) + * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2019 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 @@ -990,6 +992,11 @@ 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 @@ -1161,6 +1168,18 @@ var version_history = new 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(); + } + } } } @@ -1942,6 +1961,14 @@ function initOnDocumentReady() { if ("${BUILD_MODULE_USER_MANAGEMENT}" == "ENABLED") { caosdb_modules.register(user_management); } + + if ("${BUILD_MODULE_SHOW_ID_IN_LABEL}" == "ENABLED") { + // Remove the "display: none;" rule from the caosdb-label-id class + [...document.styleSheets] + .map(s => {return [...s.cssRules].find(x=> x.selectorText=='.caosdb-label-id')}) + .filter(Boolean) + .forEach( rule => rule.style.removeProperty("display")); + } } @@ -2000,4 +2027,4 @@ class _CaosDBModules { var caosdb_modules = new _CaosDBModules() -$(document).ready(initOnDocumentReady); \ No newline at end of file +$(document).ready(initOnDocumentReady); diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index a09494e764b4735aa6ba5090f9fe048c194f0afc..82de9e416c2e28f21cd3f386cc6e04419284dc5f 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -5,6 +5,10 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2022 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 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 @@ -146,11 +150,14 @@ </xsl:for-each> </xsl:if> </span> - <a class="caosdb-label-name" title="Open this Entity separately."> + <a class="caosdb-label-link" title="Open this Entity separately."> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath, @id)"/> </xsl:attribute> - <xsl:value-of select="@name"/> + <span class="caosdb-label-id"><xsl:value-of select="@id"/></span> + <span class="caosdb-label-name"> + <xsl:value-of select="@name"/> + </span> </a> <div class="caosdb-v-entity-header-buttons-list ms-auto"> <xsl:apply-templates mode="entity-heading-attributes-state" select="State"> diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl index 59df37d3b9a77225845b3a0700c70436946a54e5..8c53b3bc09dbd9c5f9dd120c7b3b9fb3e483b99f 100644 --- a/src/core/xsl/navbar.xsl +++ b/src/core/xsl/navbar.xsl @@ -48,7 +48,7 @@ <xsl:with-param name="username"><xsl:value-of select="/Response/@username"/></xsl:with-param> </xsl:call-template> </xsl:if> - <nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top mb-2 flex-column"> + <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"> diff --git a/src/doc/conf.py b/src/doc/conf.py index 8e627dd0c26f9d760c549abfab4cbc824baa9912..a9b28b9305b67cb552a5f279b4ef2f330ee7929c 100644 --- a/src/doc/conf.py +++ b/src/doc/conf.py @@ -22,13 +22,13 @@ import sphinx_rtd_theme project = 'caosdb-webui' -copyright = '2020, IndiScale GmbH' +copyright = '2022, IndiScale GmbH' author = 'Daniel Hornung' # The short X.Y version -version = '0.X.Y' +version = '0.6.1' # The full version, including alpha/beta/rc tags -release = '0.x.y-beta-rc2' +release = '0.6.1-SNAPSHOT' # -- General configuration --------------------------------------------------- diff --git a/src/doc/extension/references.rst b/src/doc/extension/references.rst index 63c551612e5e9d807846595b6c5e458bc5096615..c41d907de57b658194e062abcd734b41ef88ab9b 100644 --- a/src/doc/extension/references.rst +++ b/src/doc/extension/references.rst @@ -23,16 +23,15 @@ the basic structure of the module should look like // Has to be called ``resolve`` and has to take exactly one // string parameter: the id of the referenced entity. this.resolve = async function (id) { - /* - * find the string that the reference should be resolved to, - * e.g., from the value of the entity's properties. - */ - return {"text": new_reference_text} + /* + * find the string that the reference should be resolved to, + * e.g., from the value of the entity's properties. + */ + return {"text": new_reference_text} } } An example is located in -``caosdb-webui/src/ext/js/person_reference_resolver.js``. It resolves -any reference to a ``Person`` Record to the value of its ``firstname`` -and ``lastname`` properties separated by a space and is active by -default. +``caosdb-webui/src/core/js/reference_resolver/caosdb_default_person.js``. It +resolves any reference to a ``Person`` Record to the value of its ``firstname`` +and ``lastname`` properties separated by a space and is active by default. diff --git a/test/core/js/modules/caosdb.js.js b/test/core/js/modules/caosdb.js.js index 8df5e2f9c2b933cd7b678286295961f2c73d7113..b9925f28aa8eee3a828b17ea2f0dcd6b050f2711 100644 --- a/test/core/js/modules/caosdb.js.js +++ b/test/core/js/modules/caosdb.js.js @@ -19,7 +19,15 @@ QUnit.module("caosdb.js", { }, before: function(assert) { - var done = assert.async(3); + var done = assert.async(4); + + // load entity.xsl + var qunit_obj = this; + _retrieveEntityXSL().then(function(xsl) { + qunit_obj.entityXSL = xsl + done(); + }); + this.setTestDocument("x", done, ` <Response> <Record name="nameofrecord"> @@ -82,7 +90,7 @@ QUnit.module("caosdb.js", { </Record> </Response>`); - + // Test document for unset references this.setTestDocument("unsetReferencesTest", done, ` <Response> @@ -483,59 +491,37 @@ QUnit.test("_constructXpaths", function (assert) { QUnit.test("getPropertyValues", function (assert) { const test_response = str2xml(` -<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8"> - <Query string="select Campaign.responsible.firstname from icecore" results="8"> - <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e ))))) from (entity icecore) <EOF>)</ParseTree> - <Role/> - <Entity>icecore</Entity> - <Selection> - <Selector name="Campaign.responsible.firstname"/> - </Selection> - </Query> +<Response> <Record id="6525" name="Test_IceCore_1"> - <Permissions/> <Property datatype="Campaign" id="6430" name="Campaign"> <Record id="6516" name="Test-2020_Camp1"> - <Permissions/> <Property datatype="REFERENCE" id="168" name="responsible"> <Record id="6515" name="Test_Scientist"> - <Permissions/> <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> 1.34 - <Permissions/> </Property> <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> 2 - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> <Record id="6526" name="Test_IceCore_2"> - <Permissions/> <Property datatype="Campaign" id="6430" name="Campaign"> <Record id="6516" name="Test-2020_Camp1"> - <Permissions/> <Property datatype="REFERENCE" id="168" name="responsible"> <Record id="6515" name="Test_Scientist"> - <Permissions/> <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> 3 - <Permissions/> </Property> <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> 4.8345 - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> </Response>`); @@ -545,6 +531,57 @@ QUnit.test("getPropertyValues", function (assert) { [["6525" ,"1.34", "2"], ["6526", "3", "4.8345"]]); }); +QUnit.test("getPropertyValues - with list of references", function (assert) { + const test_response = str2xml(` +<Response> + <Record id="7393"> + <Version id="7f04ebc3a09d43f8711371a1d62905e5fc6af80f" head="true" /> + <Parent id="7392" name="PathObject" /> + <Property datatype="LIST<MapObject>" id="7391" name="MapObject"> + <Value> + <Record id="7394" name="Object-0"> + <Version id="4c3b4a7ef4abc4d3b6045968f3b5f028d82baab2" head="true" /> + <Property datatype="DOUBLE" id="7389" name="longitude" importance="FIX" unit="°"> + -44.840238182501864 + </Property> + <Property datatype="DOUBLE" id="7390" name="latitude" importance="FIX" unit="°"> + 83.98152416509532 + </Property> + </Record> + </Value> + <Value> + <Record id="7395" name="Object-1"> + <Version id="42fbe0c9be68c356f81f590cddbdd3d5fc17cba4" head="true" /> + <Property datatype="DOUBLE" id="7389" name="longitude" importance="FIX" unit="°"> + -35.60247552143245 + </Property> + <Property datatype="DOUBLE" id="7390" name="latitude" importance="FIX" unit="°"> + 73.86388403927366 + </Property> + </Record> + </Value> + <Value> + <Record id="7396" name="Object-2"> + <Version id="45b71028261061e94ae198eaaa66af0612004173" head="true" /> + <Property datatype="DOUBLE" id="7389" name="longitude" importance="FIX" unit="°"> + -42.429495631197724 + </Property> + <Property datatype="DOUBLE" id="7390" name="latitude" importance="FIX" unit="°"> + 74.95382063506622 + </Property> + </Record> + </Value> + </Property> + </Record> +</Response>`); + + assert.propEqual( + getPropertyValues(test_response, [["id"], ["", "latitude"],["", + "longitude"]]), [["7393", ["83.98152416509532", "73.86388403927366", + "74.95382063506622"], ["-44.840238182501864", "-35.60247552143245", + "-42.429495631197724"]]]); +}); + // Test for bug 103 // If role is File when creating XML for entities, checksum, path and size must be given. QUnit.test("unset_file_attributes", function(assert) { @@ -566,3 +603,61 @@ QUnit.test("unset_file_attributes", function(assert) { undefined); assert.equal(xml2str(res3), "<File id=\"103\" name=\"test\" path=\"testfile.txt\" checksum=\"blablabla\" size=\"0\"/>"); }); + +QUnit.test("getPropertyFromElement", async function(assert) { + var data = await $.ajax({ + cache: true, + dataType: 'xml', + url: "xml/test_case_list_of_myrecordtype.xml", + }); + console.log(this.entityXSL); + var xsl = injectTemplate(this.entityXSL, '<xsl:template match="/"><ul><xsl:apply-templates select="Property" mode="entity-body"/></ul></xsl:template>'); + var params = { + entitypath: "/entitypath/" + }; + var ret = xslt(data, xsl, params); + assert.ok(ret); + assert.propEqual(getPropertyFromElement(ret.firstElementChild), { + "datatype": "LIST<MyRecordType>", + "description": undefined, + "html": {}, + "id": "149315", + "list": true, + "listDatatype": "MyRecordType", + "name": "MyRecordType", + "reference": true, + "unit": undefined, + "value": [ + "167510", + "", + "167546", + "167574", + "167625", + "167515", + "167441", + "167596", + "167249", + "167632", + "167593", + "167321", + "167536", + "167389", + "167612", + "167585", + "167228", + "167211", + "167414", + "167282", + "167409", + "167637", + "167487", + "167328", + "167572", + "167245", + "167615", + "167301", + "167466" + ] + }); + +}); diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js index 34707c0bae5b128dce33ff28f584cac605579ad6..04ec35a6fc14cbad81c731908e28f788999c3563 100644 --- a/test/core/js/modules/entity.xsl.js +++ b/test/core/js/modules/entity.xsl.js @@ -128,12 +128,13 @@ QUnit.test("Entities have a caosdb-annotation-section", function(assert) { QUnit.test("LIST Property", function(assert) { var done = assert.async(); var entityXSL = this.entityXSL; - assert.expect(2); + assert.expect(4); $.ajax({ cache: true, dataType: 'xml', url: "xml/test_case_list_of_myrecordtype.xml", }).done(function(data, textStatus, jdXHR) { + console.log(entityXSL); var xsl = injectTemplate(entityXSL, '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>'); var params = { entitypath: "/entitypath/" @@ -141,6 +142,8 @@ QUnit.test("LIST Property", function(assert) { var ret = xslt(data, xsl, params); assert.ok(ret); assert.equal(ret.firstChild.className, "caosdb-value-list", "property value contains a list.") + assert.equal($(ret.firstChild).find(".caosdb-f-property-single-raw-value").length, 29, "29 values in the list"); + assert.equal($(ret.firstChild).find(".caosdb-f-reference-value").length, 28, "28 reference values in the list"); }).always(function() { done(); }); diff --git a/test/core/js/modules/ext_autocomplete.js.js b/test/core/js/modules/ext_autocomplete.js.js index 96cab766fb848b74b04677f9b3312b574b9a3844..aaefd228705e12a0c47bf47f1a4e1ce7936d58f6 100644 --- a/test/core/js/modules/ext_autocomplete.js.js +++ b/test/core/js/modules/ext_autocomplete.js.js @@ -88,6 +88,26 @@ QUnit.test("searchPost", async function(assert) { assert.propEqual(result, expected); }); +QUnit.test("searchPost webui#170", async function(assert) { + // https://gitlab.com/caosdb/caosdb-webui/-/issues/170 + // Autocompletion for "IS REFERENCED BY" leads to query syntax error + const resultsFromServer = ["REFERENCED BY"]; + const origJQElement = [{ + selectionEnd: 24, + value: "FIND Event WHICH IS REFE", + }]; + + const expected = [ + { + "html": "REFERENCED BY", + "text": "FIND Event WHICH IS REFERENCED BY" + }, + ]; + + const result = ext_autocomplete.searchPost(resultsFromServer, origJQElement); + assert.propEqual(result, expected); +}); + QUnit.test("class", function(assert) { assert.ok(ext_autocomplete.switch_on_completion , "toggle available"); assert.ok(ext_autocomplete.switch_on_completion() , "toggle runs"); diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 9b6b01022d8106153d50eaa906a0ce33803a8dc3..3e55506b9abc8742ae59bf958febd9f63f968ba9 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -43,6 +43,31 @@ QUnit.module("ext_map.js", { lng + `</div> <div class="caosdb-f-property-value">5.23</div> </div> +</div>`; + // This one has lists of lat/lng. + this.test_map_entity_2 = ` +<div class="caosdb-entity-panel caosdb-properties"> + <div class="caosdb-id">1234</div> + <div class="list-group-item caosdb-f-entity-property"> + <div class="caosdb-property-datatype">LIST</div> + <div class="caosdb-property-name">` + + lat + `</div> + <div class="caosdb-f-property-value"> + <div class="caosdb-value-list"><div + class="caosdb-f-property-single-raw-value">1.231</div> + <div class="caosdb-f-property-single-raw-value">1.232</div></div> + </div> + </div> + <div class="list-group-item caosdb-f-entity-property"> + <div class="caosdb-property-datatype">LIST</div> + <div class="caosdb-property-name">` + + lng + `</div> + <div class="caosdb-f-property-value"> + <div class="caosdb-value-list"><div + class="caosdb-f-property-single-raw-value">5.231</div> + <div class="caosdb-f-property-single-raw-value">5.232</div></div> + </div> + </div> </div>`; }, beforeEach: function (assert) { @@ -51,7 +76,7 @@ QUnit.module("ext_map.js", { }); QUnit.test("availability", function (assert) { - assert.equal(caosdb_map.version, "0.4", "test version"); + assert.equal(caosdb_map.version, "0.4.1", "test version"); assert.ok(caosdb_map.init, "init available"); }); @@ -125,6 +150,20 @@ QUnit.test("create_map_panel", function (assert) { assert.ok($(panel).hasClass("container"), "has class container"); }); +QUnit.test("init_map_panel", function (assert) { + assert.ok(caosdb_map.init_map_panel, "available"); + const dummy_nav = $("<nav/>"); + $(document.body).append(dummy_nav); + let panel = caosdb_map.init_map_panel(true); + + assert.ok($(panel).is(":visible"), "Panel is visible"); + + panel = caosdb_map.create_map_panel(true); + assert.notOk($(panel).is(":visible"), "Panel is not visible"); + + dummy_nav.remove(); +}); + QUnit.test("create_map_view", function (assert) { var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0], { "select": true, @@ -163,7 +202,6 @@ QUnit.test("create_map_view", function (assert) { map = caosdb_map.create_map_view(map_panel[0], view_config); - console.log(map_panel[0]); assert.ok(map instanceof L.Map, "map instance created"); assert.equal($(map_panel).find(".leaflet-container").length, 1, "map_panel has .leaflet-container child"); assert.ok(map._crs instanceof L.Proj.CRS, "map has special crs"); @@ -194,8 +232,24 @@ QUnit.test("create_entity_markers", function (assert) { assert.notOk(markers[0].getPopup(), "no popup"); // with popup - var markers = caosdb_map.create_entity_markers(entities, datamodel, () => "popup"); + markers = caosdb_map.create_entity_markers(entities, datamodel, () => "popup"); assert.ok(markers[0].getPopup(), "has popup"); + + + // test with list of lat/lng + entities = $(this.test_map_entity_2).toArray(); + markers = caosdb_map.create_entity_markers(entities, datamodel); + assert.equal(markers.length, 2, "has two marker"); + assert.ok(markers[0] instanceof L.Marker, "is marker"); + assert.ok(markers[1] instanceof L.Marker, "is marker"); + + latlng = markers[0]._latlng; + assert.equal(latlng.lat, "1.231", "latitude set"); + assert.equal(latlng.lng, "5.231", "longitude set"); + + latlng = markers[1]._latlng; + assert.equal(latlng.lat, "1.232", "latitude set"); + assert.equal(latlng.lng, "5.232", "longitude set"); }); @@ -249,9 +303,9 @@ QUnit.test("_get_with_POV ", function (assert) { assert.equal(caosdb_map._get_with_POV( []), "", "no POV"); assert.equal(caosdb_map._get_with_POV( - ["lol"]), " WITH lol ", "single POV"); + ["lol"]), " WITH \"lol\" ", "single POV"); assert.equal(caosdb_map._get_with_POV( - ["lol", "hi"]), " WITH lol WITH hi ", "with two POV"); + ["lol", "hi"]), " WITH \"lol\" WITH \"hi\" ", "with two POV"); }); @@ -260,71 +314,49 @@ QUnit.test("_get_select_with_path ", function (assert) { assert.throws(() => caosdb_map._get_select_with_path(this.datamodel, []), /Supply at least a RecordType./, "missing value"); assert.equal(caosdb_map._get_select_with_path( this.datamodel, - ["RealRT"]), "SELECT parent,latitude,longitude FROM ENTITY RealRT WITH latitude AND longitude ", "RT only"); + ["RealRT"]), "SELECT parent,latitude,longitude FROM ENTITY \"RealRT\" WITH ( \"latitude\" AND \"longitude\" ) ", "RT only"); assert.equal(caosdb_map._get_select_with_path( this.datamodel, - ["RealRT", "prop1"]), "SELECT parent,prop1.latitude,prop1.longitude FROM ENTITY RealRT WITH prop1 WITH latitude AND longitude ", "RT with one prop"); + ["RealRT", "prop1"]), "SELECT parent,prop1.latitude,prop1.longitude FROM ENTITY \"RealRT\" WITH \"prop1\" WITH ( \"latitude\" AND \"longitude\" ) ", "RT with one prop"); assert.equal(caosdb_map._get_select_with_path( this.datamodel, - ["RealRT", "prop1", "prop2"]), "SELECT parent,prop1.prop2.latitude,prop1.prop2.longitude FROM ENTITY RealRT WITH prop1 WITH prop2 WITH latitude AND longitude ", "RT with two props"); + ["RealRT", "prop1", "prop2"]), "SELECT parent,prop1.prop2.latitude,prop1.prop2.longitude FROM ENTITY \"RealRT\" WITH \"prop1\" WITH \"prop2\" WITH ( \"latitude\" AND \"longitude\" ) ", "RT with two props"); }); QUnit.test("_get_leaf_prop", async function (assert) { const test_response = str2xml(` -<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8"> - <Query string="select Campaign.responsible.firstname from icecore" results="8"> - <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e ))))) from (entity icecore) <EOF>)</ParseTree> - <Role/> - <Entity>icecore</Entity> - <Selection> - <Selector name="Campaign.responsible.firstname"/> - </Selection> - </Query> +<Response> <Record id="6525" name="Test_IceCore_1"> - <Permissions/> <Property datatype="Campaign" id="6430" name="Campaign"> <Record id="6516" name="Test-2020_Camp1"> - <Permissions/> <Property datatype="REFERENCE" id="168" name="responsible"> <Record id="6515" name="Test_Scientist"> - <Permissions/> <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> 1.34 - <Permissions/> </Property> <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> 2 - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> <Record id="6526" name="Test_IceCore_2"> - <Permissions/> <Property datatype="Campaign" id="6430" name="Campaign"> <Record id="6516" name="Test-2020_Camp1"> - <Permissions/> <Property datatype="REFERENCE" id="168" name="responsible"> <Record id="6515" name="Test_Scientist"> - <Permissions/> <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> 3 - <Permissions/> </Property> <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> 4.8345 - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> - <Permissions/> </Property> </Record> </Response>`); @@ -359,6 +391,85 @@ QUnit.test("_get_leaf_prop", async function (assert) { }); + +QUnit.test("_get_leaf_prop - list of referenced map entities", async function (assert) { + const test_response = str2xml(` +<Response> + <Record id="7393"> + <Version id="7f04ebc3a09d43f8711371a1d62905e5fc6af80f" head="true" /> + <Parent id="7392" name="PathObject" /> + <Property datatype="LIST<MapObject>" id="7391" name="MapObject"> + <Value> + <Record id="7394" name="Object-0"> + <Version id="4c3b4a7ef4abc4d3b6045968f3b5f028d82baab2" head="true" /> + <Property datatype="DOUBLE" id="7389" name="longitude" importance="FIX" unit="°"> + -44.840238182501864 + </Property> + <Property datatype="DOUBLE" id="7390" name="latitude" importance="FIX" unit="°"> + 83.98152416509532 + </Property> + </Record> + </Value> + <Value> + <Record id="7395" name="Object-1"> + <Version id="42fbe0c9be68c356f81f590cddbdd3d5fc17cba4" head="true" /> + <Property datatype="DOUBLE" id="7389" name="longitude" importance="FIX" unit="°"> + -35.60247552143245 + </Property> + <Property datatype="DOUBLE" id="7390" name="latitude" importance="FIX" unit="°"> + 73.86388403927366 + </Property> + </Record> + </Value> + <Value> + <Record id="7396" name="Object-2"> + <Version id="45b71028261061e94ae198eaaa66af0612004173" head="true" /> + <Property datatype="DOUBLE" id="7389" name="longitude" importance="FIX" unit="°"> + -42.429495631197724 + </Property> + <Property datatype="DOUBLE" id="7390" name="latitude" importance="FIX" unit="°"> + 74.95382063506622 + </Property> + </Record> + </Value> + </Property> + </Record> +</Response> +`); + var leaves = caosdb_map._get_leaf_prop(test_response, 1, this.datamodel) + + assert.equal(Object.keys(leaves).length, 1, "number of records"); + var leave = leaves["7393"]; + assert.ok(leave, "has entity"); + assert.deepEqual(leave, [["83.98152416509532", + "73.86388403927366", "74.95382063506622"], [ "-44.840238182501864", + "-35.60247552143245", "-42.429495631197724" ]]); + + assert.equal( + caosdb_map._get_toplvl_rec_with_id(test_response, "7393")["id"], + "7393", + "number of records"); + + caosdb_map._set_subprops_at_top( + test_response, 1, this.datamodel, { + "7393": [["83.98152416509532", "73.86388403927366", + "74.95382063506622"], [ "-44.840238182501864", + "-35.60247552143245", "-42.429495631197724" ]] }) + assert.equal($(test_response).find(`[name='longitude']`).length, + 4, + "number lng props"); + assert.equal($(test_response).find(`[name='latitude']`).length, + 4, + "number lat props"); + // after transforming, the long/lat props should be accessible + var html_ents = await transformation.transformEntities(test_response); + assert.deepEqual( + getProperty(html_ents[0], "longitude"), [ "-44.840238182501864", + "-35.60247552143245", "-42.429495631197724" ], + "longitude of first rec"); + +}); + QUnit.test("_get_id_POV", function (assert) { assert.equal(caosdb_map._get_id_POV([]), "WITH ", "no POV"); assert.equal(caosdb_map._get_id_POV([5]), "WITH id=5", "one id"); diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js index 54e06d33d5f1c33781efe11802a7fbfc5ba44d89..905b90674c6fdbeb683e42900960d646a0d0a315 100644 --- a/test/core/js/modules/ext_references.js.js +++ b/test/core/js/modules/ext_references.js.js @@ -104,7 +104,7 @@ QUnit.test("is_child", function(assert){ }); QUnit.test("get_person_str", function(assert){ - assert.ok(person_reference.get_person_str); + assert.ok(caosdb_default_person_reference.get_person_str); }); QUnit.test("update_visible_references_without_summary", async function(assert){ diff --git a/test/core/js/modules/form_panel.js.js b/test/core/js/modules/form_panel.js.js index bc8343d4a65233e025039e7476861fb998c2abbc..be4c4ad8e66402adedf8c386ff086be4d7cb7955 100644 --- a/test/core/js/modules/form_panel.js.js +++ b/test/core/js/modules/form_panel.js.js @@ -55,7 +55,7 @@ QUnit.test("create_show_form_callback ", function (assert) { help: help_text, }, ], }; - 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() }); diff --git a/test/core/xml/test_case_list_of_myrecordtype.xml b/test/core/xml/test_case_list_of_myrecordtype.xml index b67c857e22a983774b0ae1ba7e88d9ccd2515de8..63587832758049c6de22055225c9dbb9acb7ff48 100644 --- a/test/core/xml/test_case_list_of_myrecordtype.xml +++ b/test/core/xml/test_case_list_of_myrecordtype.xml @@ -24,6 +24,7 @@ <Property id="149315" name="MyRecordType" datatype="LIST<MyRecordType>" importance="FIX"> <Value>167510</Value> + <Value></Value> <Value>167546</Value> <Value>167574</Value> <Value>167625</Value>