diff --git a/CHANGELOG.md b/CHANGELOG.md index f3fcda0ed89756011e24232c5c0009b1168b9c4c..6ae4be1462bb86c43cfa3a48782cb3910a30fc29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features, dependecies etc.) -* Map (v0.1) - * adds a button to the navbar which toggles a map in the top of the main - panel. The map currently shows all entities on the current page and can be used to select an area in the map and generate a query filter for this area which is can be openend in the query panel. - * see the caosdb_map module in [ext_map.js](./src/core/js/ext_map.js) - * the default configuration is located in the module itself. - * the module needs an additional [ext.json](./conf/ext/json/ext_map.json) - defines at least the tiling server. The tiling server is not configured - in the default config because this would require the caosdb maintainers - to enforce the respective usage policies of the tiling server providers. - * test data for your server can be generated with [map_test_data.py](./misc/map_test_data.py). +* Map (v0.3) + * Adds a button to the navbar which toggles a map in the top of the main + panel. The map currently shows all known entities which have geolocation + properties and highlights those which are on the current page. Users can + select an area in the map and generate a query filter for this area which + is can be openend in the query panel. + * See the caosdb_map module in [ext_map.js](./src/core/js/ext_map.js) + * The default configuration is located in the module itself. + * The module needs an additional [ext_map.json](./conf/ext/json/ext_map.json) + which defines at least the tiling server. The tiling server is not + configured in the default config because this would require the caosdb + maintainers to enforce the respective usage policies of the tiling server + providers. + * Test data for your server can be generated with + [map_test_data.py](./misc/map_test_data.py). + * The map module supports different map projections, e.g. Spherical + Mercartor and projections for the polar regions. The active projection + can changed by the user with a button. * navbar module (v0.1) * A collection of helper functions for adding stuff to the navbar. * caosdb_utils module (v0.1) @@ -26,12 +34,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * New Dependency: leaflet-1.5-1 * for the map * New Dependency: loglevel-1.6.4 - * our new logging framework. Any new logging should be done with this framework. + * Our new logging framework. Any new logging should be done with this + framework. ### Changed (for changes in existing functionality) -* in [entity.xsl](./src/core/xsl/entity.xsl): an emtpy property value (a `NULL` property) did not produce any `<span>` element with class `caosdb-property-text-value`. This caused the current implementation of `getPropertyFromElement` in [caosdb.js](./src/core/js/caosdb.js) to return the unit of the property as the value. The new implementation produces an empty `<span>` (no child text node) which is more appropriate and also fixes the buggy `getPropertyFromElement` without touching it. -* in [webcaosdb.js](./src/core/js/webcaosdb.js), `markdown` module: The markdown module is very generell and small now. The logic for converting comments (aka CommentAnnotations) to markdown is implemented in the `annotation` module now (which uses the `markdown` module as back-end, tho). +* The old `caosdb-property-row` CSS class has been replaced by + `caosdb-v-property-row` for styling and `caosdb-f-property` for functional + needs. +* In [ext_xls_download.js](./src/core/js/ext_xls_download.js): Complete rewrite + of the module. The generation of the TSV table is done in this module now, + instead of generating it with xsl (in [query.xsl](./src/core/xsl/query.xsl)). + Also it is generated on demand. +* In [ext_references.js](./src/core/js/ext_references.js): Updated + ext_references to v0.2. The new version can also generate and show summaries + of `LIST` properties. +* In [entity.xsl](./src/core/xsl/entity.xsl): an emtpy property value (a `NULL` + property) did not produce any `<span>` element with class + `caosdb-property-text-value`. This caused the current implementation of + `getPropertyFromElement` in [caosdb.js](./src/core/js/caosdb.js) to return + the unit of the property as the value. The new implementation produces an + empty `<span>` (no child text node) which is more appropriate and also fixes + the buggy `getPropertyFromElement` without touching it. +* In [webcaosdb.js](./src/core/js/webcaosdb.js), `markdown` module: The + markdown module is very generell and small now. The logic for converting + comments (aka CommentAnnotations) to markdown is implemented in the + `annotation` module now (which uses the `markdown` module as back-end, tho). * updated QUnit test framework to 2.9.2 ### Deprecated (for soon-to-be removed features) diff --git a/misc/list_references_test_data.py b/misc/list_references_test_data.py new file mode 100755 index 0000000000000000000000000000000000000000..68371df2f3a5db4d0da5e26abba432ef846b5898 --- /dev/null +++ b/misc/list_references_test_data.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +(C) Copyright IndiScale GmbH 2019 +""" + +import caosdb +import random + +caosdb.execute_query("FIND Referenc*").delete() + +# data model +datamodel = caosdb.Container() +datamodel.extend([ + caosdb.RecordType("Referenced"), + caosdb.RecordType( + "Referencing" + ).add_property("Referenced", datatype=caosdb.LIST("Referenced")), +]) + +datamodel.insert() + + +# test data +testdata = caosdb.Container() + +for i in range(100): + testdata.append( + caosdb.Record("ReferenceObject-{}".format(i) + ).add_parent("Referenced") + ) + +testdata.insert(); +caosdb.Record().add_parent( + "Referencing" + ).add_property("Referenced", + datatype=caosdb.LIST("Referenced"), + value=testdata + ).insert() + diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index cc704be6cdd816cfe3214116a07275b3118bc363..0c8a1a5eb7b01efa947bafd7d17955582eceec44 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -269,11 +269,24 @@ h5 { .caosdb-v-edit-list { padding-left: 0px; } + +.caosdb-v-editmode-existing { + height: 320.7px; + overflow-y: auto; +} + .caosdb-v-edit-panel { position: sticky; - top: 20px; + top: 57px; padding: 0px; + margin-top: 5px; + margin-left: 5px; width: unset; + height: 800px; +} + +.caosdb-v-editmode-btngroup { + padding-bottom: 15px; } .caosdb-prop-list-group>.list-group-item { @@ -444,6 +457,11 @@ h5 { border-left: 0px solid #7c7c7c; } +.caosdb-paging-panel { + padding-left: 0px; + padding-right: 0px; +} + .caosdb-pagination { margin: 5px 15px; } @@ -478,7 +496,7 @@ h5 { 100% { transform: rotate(360deg); } } -.caosdb-property-row { +.caosdb-v-property-row { animation: appear 0.5s 1; padding: 0.3ex 1em; } @@ -514,11 +532,6 @@ input[type="file"] { min-height: 22px; } -.caosdb-v-edit-panel { - max-height: 80vh; - overflow-y: auto; -} - footer { background-color: lightgrey; padding: 0.5em; diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js index 85032d8e973f64f94565fff2e1b3187f735d9021..c96881e230ef16ff929cc8fdffdf36e01a9df722 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -348,12 +348,17 @@ function getParents(element) { /** * Find all elements that fulfill a condition. * Don't traverse elements if except condition is matched. - * @param element The start node. - * @param condition The condition. - * @param except The stop condition. + * + * @param {HTMLElement} element - The start node. + * @param {function} condition - A filter callback which returns `true` for + * each element which should be included. + * @param {function} except - A filter callback which returns `true` for an + * element when the traversing should stop. + * + * @returns {HTMLElement[]} an array of HTMLElements */ function findElementByConditions(element, condition, except) { - let found = [] + let found = []; let echild = element.children; for (var i = 0; i < echild.length; i++) { @@ -400,20 +405,43 @@ function getPropertyName(element) { } /** - * Return a list of property objects from the dom property element. - * @param propertyelement: A HTMLElement identifying the property element. - * @param names: a map of names tracking the count of usage for each name (optional) + * A JSON representation of an entities property. + * + * TODO description, importance + * + * @type {EntityProperty} + * @property {string} id + * @property {string} name + * @property {number} duplicateIndex - Number of properties in the entity with + * the same name. + * @property {HTMLElement} html - An HTML representation of the property. + * @property {string} datatype + * @property {boolean} reference - has a reference datatype? + * @property {string|string[]} value + * @property {string} unit + * @property {boolean} list - has a list datatype? + * @property {string} listDatatype - the datatype of the list elements + */ + +/** + * Return a json property object. + * + * @param {HTMLElement} propertyelement - A HTMLElement representing the property. + * property element. + * @param {object} [names] - a map of names tracking the count of usage for + * each name (Default: undefined) * - * TODO: Retrieval when using list element preview is currently broken. + * @return {Property} **/ function getPropertyFromElement(propertyelement, names = undefined) { - let property = new Object({}); + let property = {}; let valel = propertyelement.getElementsByClassName("caosdb-property-value")[0]; let dtel = propertyelement.getElementsByClassName("caosdb-property-datatype")[0]; let idel = propertyelement.getElementsByClassName("caosdb-property-id")[0]; let unitel = valel.getElementsByClassName("caosdb-unit")[0]; + property.html = propertyelement; // name property.name = getPropertyName(propertyelement); @@ -484,15 +512,13 @@ function getPropertyFromElement(propertyelement, names = undefined) { let listel; if (property.reference) { // list of referernces - // TODO: Fix list preivew here. Fixed, but untested. - listel = findElementByConditions(valel, x => x.classList.contains("caosdb-resolvable-reference"), + 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 - // TODO: Fix list preivew here. Fixed, but untested. listel = findElementByConditions(valel, x => x.classList.contains("list-group-item"), x => x.classList.contains("caosdb-preview-container")); for (var j = 0; j < listel.length; j++) { @@ -501,8 +527,6 @@ function getPropertyFromElement(propertyelement, names = undefined) { } } else if (property.reference) { // reference datatypes - // let el = findElementByConditions(valel, x => x.classList.contains("caosdb-id"), - // x => x.classList.contains("caosdb-preview-container")); property.value = getIDfromHREF(valel.getElementsByTagName("a")[0]); } else { // all other datatypes @@ -515,13 +539,15 @@ function getPropertyFromElement(propertyelement, names = undefined) { } /** - * Get the properties from an entity. - * @param element The element holding the entity. - * @return a list of dom elements containing the properties. + * Get all the properties from an entity. + * + * @param {HTMLElement} element The element holding the entity. + * + * @return {HTMLElement[]} a list of properties in HTML representation. */ function getPropertyElements(element) { return findElementByConditions(element, - x => x.classList.contains("caosdb-property-row"), + x => x.classList.contains("caosdb-f-entity-property"), x => x.classList.contains("caosdb-preview-container")); } @@ -564,8 +590,6 @@ function getProperties(element) { * @param valueelement The dom element where the text content is to be set. * @param property The new property. * @param propold The old property belonging to valueelement. - * - * TODO: Server string is hardcoded. */ function setPropertySafe(valueelement, property, propold) { const serverstring = connection.getBasePath() + "Entity/"; diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index 5b11866a4a3d50ee6dc2c3ae133612a4b20836ff..692ae79ab9847b62ef06a3349e2ac9bf2bebda93 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -330,54 +330,57 @@ var edit_mode = new function() { * ent_element : {HTMLElement} entity in view mode */ this.getProperties = function(ent_element) { - var properties = []; - for (var element of $(ent_element).find('.caosdb-property-row')) { - - var valfield = $(element).find(".caosdb-property-value"); - var editfield = $(element).find(".caosdb-property-edit-value"); - var property = getPropertyFromElement(element); - - // LISTs need to be handled here - if (property.list == true) { - // TODO: unit missing - property.value = []; - if (this.checkForDatatypeList(property.listDatatype, - ["TEXT","DATE","DOUBLE","INTEGER","BOOLEAN","FILE"])) { - // LOOP over elements of editfield.find(":input") - for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input")) { - property.value.push($(singleelement).val()); - } - } else if (property.datatype == "DATETIME") { - throw ("Lists of DATETIME currently not supported."); - } else if (property.reference) { - // LOOP over elements of editfield.find("select") - for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input")) { - property.value.push(singleelement.selectedOptions[0].value); + const properties = []; + if (ent_element) { + const prop_elements = getPropertyElements(ent_element); + for (var element of prop_elements) { + + var valfield = $(element).find(".caosdb-property-value"); + var editfield = $(element).find(".caosdb-property-edit-value"); + var property = getPropertyFromElement(element); + + // LISTs need to be handled here + if (property.list == true) { + // TODO: unit missing + property.value = []; + if (this.checkForDatatypeList(property.listDatatype, + ["TEXT","DATE","DOUBLE","INTEGER","BOOLEAN","FILE"])) { + // LOOP over elements of editfield.find(":input") + for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input")) { + property.value.push($(singleelement).val()); + } + } else if (property.datatype == "DATETIME") { + throw ("Lists of DATETIME currently not supported."); + } else if (property.reference) { + // LOOP over elements of editfield.find("select") + for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input")) { + property.value.push(singleelement.selectedOptions[0].value); + } + } else { + throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); } } else { - throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); - } - } else { - property.unit = editfield.find(".caosdb-unit").val(); - if (this.checkForDatatypeList(property.datatype, - ["TEXT","DATE","DOUBLE","INTEGER","BOOLEAN","FILE"])) { - property.value = editfield.find(":input").val() - } else if (property.datatype == "DATETIME") { - let es = editfield.find(":input"); - if (es.length == 2) { - property.value = input2caosdbDate( - es[0].value, - es[1].value); - } else if (es[0]) { - property.value = es[0].value; + property.unit = editfield.find(".caosdb-unit").val(); + if (this.checkForDatatypeList(property.datatype, + ["TEXT","DATE","DOUBLE","INTEGER","BOOLEAN","FILE"])) { + property.value = editfield.find(":input").val() + } else if (property.datatype == "DATETIME") { + let es = editfield.find(":input"); + if (es.length == 2) { + property.value = input2caosdbDate( + es[0].value, + es[1].value); + } else if (es[0]) { + property.value = es[0].value; + } + } else if (property.reference) { + property.value = $(editfield).find("select").first()[0].selectedOptions[0].value; + } else { + throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); } - } else if (property.reference) { - property.value = $(editfield).find("select").first()[0].selectedOptions[0].value; - } else { - throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); } + properties.push(property); } - properties.push(property); } return properties; @@ -1022,7 +1025,9 @@ var edit_mode = new function() { // set listener for editable entity. hintMessages.hintMessages(app.entity); $(app.entity).find('.caosdb-annotation-section').remove(); - for (var element of $(app.entity).find('.caosdb-property-row')) { + + const prop_elements = getPropertyElements(app.entity); + for (var element of prop_elements) { edit_mode.make_property_editable(element); } app.entity.dispatchEvent(edit_mode.start_edit); diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js index aba52042292df7d2ead3b42b104db29855ca275a..67c7bfe799c8c96eb945ec9334ad87fa3f7d70f9 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -24,8 +24,11 @@ 'use strict'; /** - * caosdb_map module for displaying a geographical map which shows entities at - * their associated geo location. + * @module caosdb_map + * @version 0.3 + * + * For displaying a geographical map which shows entities at their associated + * geolocation. * * The configuration for this module has to be stored in * `conf/ext/json/ext_map.json` and comply with the {@link MapConfig} type diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index 68a52a023e49d8e9f092883817ad82b1da1c87cd..b62fcaac7a7b503fa8d96ef4fb046887efd111b1 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -1,17 +1,38 @@ -/** - * Resolve References - * Alexander Schlemmer, 11/2018 +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Alexander Schlemmer + * Copyright (C) 2019-2020 IndiScale GmbH (info@indiscale.com) + * Copyright (C) 2019-2020 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/>. + * + * ** end header */ -/*! +/* * Check if an element is out of the viewport + * * (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com * - * @param {Node} elem The element to check - * @return {Object} A set of booleans for each side of the element + * https://gomakethings.com/how-to-check-if-any-part-of-an-element-is-out-of-the-viewport-with-vanilla-js/ + * + * @param {Node} elem - The element to check + * @return {Object} A set of booleans for each side of the element. */ -// Source address: https://gomakethings.com/how-to-check-if-any-part-of-an-element-is-out-of-the-viewport-with-vanilla-js/ var isOutOfViewport = function (elem) { // Get element's bounding @@ -21,85 +42,390 @@ var isOutOfViewport = function (elem) { var out = {}; out.top = bounding.top < 0; out.left = bounding.left < 0; - out.bottom = bounding.bottom > (window.innerHeight || document.documentElement.clientHeight); - out.right = bounding.right > (window.innerWidth || document.documentElement.clientWidth); - out.any = out.top || out.left || out.bottom || out.right; - out.all = out.top && out.left && out.bottom && out.right; + out.bottom = bounding.bottom > (window.innerHeight || + document.documentElement.clientHeight); + out.right = bounding.right > + (window.innerWidth || document.documentElement.clientWidth); + out.any = + out.top || out.left || out.bottom || out.right; + out.all = out.top && + out.left && out.bottom && out.right; return out; }; + /** - * Check if an element is inside of the viewport on the vertical axis. - * - * Returns true if any of the element's bounding box's top or bottom edges are in the - * viewport. + * @module awi_references + * @version 0.1 * - * @param {HTMLElement} elem - the element to check - * @return {boolean} + * Special functionality for AWI. Should be removed from the main repository in + * the future. * + * @author Timm Fitschen */ -var is_in_viewport_vertically = function (elem) { - var out = isOutOfViewport(elem); - return !(out.top || out.bottom); +var awi_references = new function () { + + var logger = log.getLogger("awi_references"); + + + this.find_bag_of_sample = async function (id) { + return await + this._find_ice_sample_back_ref(id, "Bag"); + } + + + this.find_ice_core_of_sample = async function (id) { + return await + this._find_ice_sample_back_ref(id, "IceCore"); + } + + + this._find_ice_sample_back_ref = async function (id, rt, oldcounter) { + var counter = oldcounter + 1 || 1 + if (counter > 5) { + return null; + } + var + referencing_samples = await query( + "FIND IceSample WHICH REFERENCES " + + id); + for (const sample of referencing_samples) { + if (resolve_references.is_child(sample, rt)) { + return sample; + } else { + var ret = + await + this._find_ice_sample_back_ref(getEntityID(sample), + rt, + counter); + if (ret) { + return ret; + } + } + } + return undefined; + } + + const _stripe_re = /Stripe$/i; + this.isStripe = function (el) { + return _stripe_re.test(el.name) + } + + + this.get_icecore = async function (bag) { + var id = getEntityID(bag); + var icecore = (await query( + "SELECT name FROM IceCore WHICH REFERENCES " + + id))[0]; + var bag_number = getProperty(bag, "Number", false); + var ret = { + "data": { + "bag": bag_number + } + }; + if (!icecore) { + ret["text"] = + `${id} (Bag ${bag_number}, no Ice Core)`; + } else { + ret["text"] = `${id} (Ice Core ${getEntityName(icecore)}, Bag ${bag_number})`; + ret["data"]["icecore"] = getEntityName(icecore); + } + return ret; + } + + + this.get_bag_and_icecore = async function (sample) { + var id = + getEntityID(sample); + var bag = await awi_references.find_bag_of_sample(id); + var ret = {}; + if (!bag) { + var icecore = await awi_references.find_ice_core_of_sample(id); + if (!icecore) { + ret["text"] = `${id} (Sample w/o Bag or Ice Core)`; + } else { + ret["text"] = `${id} (Ice Core ${getEntityName(icecore)}, no Bag)`; + ret["data"] = { + "icecore": getEntityName(icecore) + }; + } + } else { + return await awi_references.get_icecore(bag); + } + return ret; + } + + + this.summarize_subsamples = function (ref_infos) { + logger.trace("enter summarize_subsamples ", ref_infos); + var icecores = {}; + for (const ref_info of ref_infos) { + const icecore_name = ref_info.data.icecore || "none"; + if (!icecores[icecore_name]) { + icecores[icecore_name] = []; + } + const bagnumber = parseInt(ref_info.data.bag, 10); + icecores[icecore_name].push(bagnumber); + } + var ret = ""; + var last = ""; + const pretty_bag_numbers = reference_list_summary + .simplify_integer_numbers; + for (const icecore_name of + Object.keys(icecores)) { + if (icecore_name === "none") { + last = + `<div class="casodb-f-resolve-reference-summary-plain">Bags without IceCore: ${pretty_bag_numbers(icecores[icecore_name])}</div>`; + } else { + ret += + `<div class="caosdb-f-resolve-reference-summary-plain">IceCore: ${icecore_name} (Bags: ${pretty_bag_numbers(icecores[icecore_name])})</div>`; + } + } + return ret.length + last.length > 0 ? + '<b>Summary</b>' + ret + last : ""; + } + + + this.summarize_box_content = function (ref_infos) { + logger.trace("enter summarize_box_content ", ref_infos); + return awi_references.summarize_subsamples(ref_infos); + } } /** - * Check if an element is inside of the viewport on the horizontal axis. - * - * This criterion is very much different from the vertical correspondent: - * It looks for the parent which contains the list scroll bar for caosdb values. - * Then it is checked whether the element's bounding box is visible inside the scroll box. + * @module reference_list_summary + * @version 0.1 * - * @param {HTMLElement} elem - the element to check - * @return {boolean} + * For generating short summaries of LIST properties. This module is used by + * the resolve_references module. * + * @author Timm Fitschen */ -var is_in_viewport_horizontally = function (elem) { - var scrollbox = elem.parentElement.parentElement; - // Check this condition only if the grand parent is a list and return true otherwise. - if (scrollbox.classList.contains("caosdb-value-list") == true) { - var boundel = elem.getBoundingClientRect(); - var boundscroll = scrollbox.getBoundingClientRect(); - var leftcrit = boundel.right > boundscroll.left; - var rightcrit = boundel.left < boundscroll.right; - return leftcrit && rightcrit; - } else { - return true; +var reference_list_summary = new function () { + + var logger = log.getLogger("reference_list_summary"); + + /** Return a condensed string representation of an array of integers. + * + * @example simplify_integer_numbers([1,2,3,5,8,9,10]) returns "1-3, 5, + * 8-10" + * + * @example simplify_integer_numbers([]) returns "" + * + * @example simplify_integer_numbers([1]) returns "1" + * + * @example simplify_integer_numbers([1,2]) returns "1, 2" because an array + * with two elements gets a special treatment. + * + * The array may be unsorted. simplify_integer_numbers([1,2,3]) returns + * "1-3" simplify_integer_numbers([2,1,3]) returns "1-3" as well + * + * The array may contain duplicates. simplify_integer_numbers([1,2,2,3]) + * returns "1-3" + * + * @param {numbers} array - unsorted array of integers, possibly with + * duplicates. @return {string} a condensed string representation of the + * array. + */ + this.simplify_integer_numbers = function (array) { + logger.trace("enter simplify_integer_numbers", array); + var set = Array.from(new Set(array)); + + if (set.length === 0) { + return "" + } else if (set.length === 1) { + return `${set[0]}`; + } + + // sort numerically + set.sort((a, b) => a - b); + + if (set.length === 2) { + return `${set[0]}, ${set[1]}`; + } + + + var ret = `${set[0]}`; + var last = undefined; + // set[0]; + + // e.g. [1,2,3,4,5,8,9,10]; + for (const next of set) { + // append '-' to summarize consecutive numbers + if (next - last === 1 && !ret.endsWith("-")) { + ret += "-"; + } + + if (next - last > 1) { + + if (ret.endsWith("-")) { + // close previous interval and start new + ret += `${last}, ${next}`; + } else { + // no previous interval, start interval. + ret += `, ${next}`; + } + } else if (next === set[set.length - 1]) { + // finish interval if next is last item + ret += next; + break; + } + + + last = next; + + } + + // e.g. "1-5, 8-10" + return ret; + } + + /** + * Generate a summary of all reference_infos by calling the callback + * function of the first reference_info in the array. + * + * The summary is appended to the summary_container if available and + * returned (for testing purposes). + * + * Returns undefined, if ref_infos is empty or does not come with a + * callback function. + * + * @param {reference_info[]} ref_infos - array of reference_infos. + * @param {HTMLElement} summary_container - the summary is appended to this + * element. + * @return {HTMLElement|string} generated summary + */ + this.generate = function (ref_infos, summary_container) { + logger.trace("enter generate", ref_infos); + if (ref_infos.length > 0 && + typeof ref_infos[0].callback === "function") { + const summary = + ref_infos[0].callback(ref_infos); + if (summary && summary_container) { + $(summary_container).append(summary); + } + logger.trace("leave generate", summary); + return summary; + } + logger.trace("leave generate, return undefined"); + return undefined; } } /** - * The resolve_references module. + * @module resolve_references + * @version 0.2 + * + * Original implementation by Alexander Schlemmer, 11/2018 + * + * @author Timm Fitschen + * @author Alexander Schlemmer */ var resolve_references = new function () { + var logger = log.getLogger("resolve_references"); + + var _scroll_timeout = undefined; + + /** + * Scroll listener calls {@link resolve_references.update_visible_references} 500 milliseconds after the + * last scroll event. + */ + var scroll_listener = () => { + if (_scroll_timeout) { + clearTimeout(_scroll_timeout); + } + _scroll_timeout = setTimeout(function () { + resolve_references.update_visible_references(); + }, 500); + }; + + + /** + * Initilize the scroll listener which triggers the resolution of the + * entity ids and trigger it for the first time in order to resolve all + * visible references. + */ this.init = function () { - this.update_visible_references(); + scroll_listener(); + + // mainly for vertical scrolling + $(window).scroll(scroll_listener); + + // for horizontal scrolling. is this still necessary when lists are + // loaded in one go? + $(".caosdb-value-list").scroll(scroll_listener); + } + + /** + * Check if an element is inside of the viewport on the vertical axis. + * + * Returns true if any of the element's bounding box's top or bottom edges are + * in the viewport. + * + * @param {HTMLElement} elem - the element to check @return {boolean} + * + */ + this.is_in_viewport_vertically = function (elem) { + var out = + isOutOfViewport(elem); + return !(out.top || out.bottom); + } + + /** Check if an element is inside of the viewport on the horizontal axis. + * + * This criterion is very much different from the vertical correspondent: It + * looks for the parent which contains the list scroll bar for caosdb values. + * Then it is checked whether the element's bounding box is visible inside the + * scroll box. + * + * @param {HTMLElement} elem - the element to check @return {boolean} + * + */ + this.is_in_viewport_horizontally = function (elem) { + var scrollbox = elem.parentElement.parentElement; + // Check this condition only if the grand parent is a list and return true + // otherwise. + if (scrollbox.classList.contains("caosdb-value-list") == + true) { + var boundel = elem.getBoundingClientRect(); + var boundscroll = scrollbox.getBoundingClientRect(); + var leftcrit = boundel.right > boundscroll.left; + var rightcrit = boundel.left < boundscroll.right; + return leftcrit && rightcrit; + } else { + return true; + } } + /** + * 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() == "firstname")[0].value + - " " + valpr.filter(valprel => valprel.name.toLowerCase() == "lastname")[0].value; + return valpr.filter(valprel => + valprel.name.toLowerCase() == "firstname")[0].value + + " " + + valpr.filter(valprel => valprel.name.toLowerCase() == + "lastname")[0].value; } /** - * Return true iff the entity has at least one parent named `rt`. + * Return true iff the entity has at least one direct parent named `par`. * - * @param {HTMLElement} el - entity in HTML representation. - * @param {string} rt - parent name. - * @returns {boolean} + * @param {HTMLElement} entity - entity in HTML representation. @param + * {string} par - parent name. @return {boolean} */ - this.isChild = function (el, rt) { - var pars = getParents(el); + this.is_child = function (entity, par) { + var pars = getParents(entity); for (const par of pars) { - if (par.name === rt) { + if (par.name === par) { return true; } } @@ -107,137 +433,240 @@ var resolve_references = new function () { } - this.find_bag_of_sample = async function (el) { - return await this._find_ice_sample_back_ref(getEntityID(el), "Bag"); + /** + * Example implementation of a function which returns a reference_info for + * referenced `ReferenceObject` entities. `ReferenceObject` is a mock-up + * entity which is used to test this module. + */ + this.get_referenced = function (entity) { + return { + "text": getEntityName(entity), + "data": { + "name": getEntityName(entity), + "number": getEntityName(entity).replace( + "ReferenceObject-", "") + }, + "callback": function (ref_infos) { + var ret = $('<div/>').append("Summary: "); + var + array = [] + for (const ref_info of ref_infos) { + array.push(parseInt(ref_info["data"]["number"], + 10)); + } + ret.append(reference_list_summary + .simplify_integer_numbers(array)); + return ret[0]; + } + }; } - this.find_ice_core_of_sample = async function (el) { - return await this._find_ice_sample_back_ref(getEntityID(el), "IceCore"); - } + this.retrieve = retrieve; + /** + * @typedef {reference_info} + * @property {string} text + * @property {function} callback - a callback function with one parameter + * (`data`) which generates a summary from the array of data objects + * which are passed to this function. + * @property {object} data - an object with properties which can be + * understood and used by the `callback` function. + */ - this._find_ice_sample_back_ref = async function (id, rt, oldcounter) { - var counter = oldcounter + 1 || 1 - if (counter > 5) { - return null; - } - var referencing_samples = await query("FIND IceSample WHICH REFERENCES " + id); - for (const sample of referencing_samples) { - if (this.isChild(sample, rt)) { - return sample; - } else { - var ret = await this._find_ice_sample_back_ref(getEntityID(sample), rt, counter); - if (ret) { - return ret; - } + /** + * Return a reference_info for an entity. + * + * TODO refactor to be configurable. @async @param {string} id - the id of + * the entity which is to be resolved. @return {reference_info} + */ + this.resolve_reference = async function (id) { + const entity = (await resolve_references.retrieve(id))[0]; + + // TODO handle multiple parents + const par = getParents(entity)[0] || {}; + + var ret = { + "text": id + }; + if (getEntityHeadingAttribute(entity, "path") !== + undefined || par.name == "Image") { + // show file name + var pths = getEntityHeadingAttribute(entity, "path") + .split("/"); + ret["text"] = pths[pths.length - 1]; + } else if (par.name === "Person") { + ret["text"] = this.get_person_str(entity); + } else if (par.name === "ExperimentSeries") { + ret["text"] = + getEntityName(entity); + } else if (par.name === "BoxType") { + ret["text"] = getEntityName(entity); + } else if (par.name === "Loan") { + var borrower = await this.retrieve(getProperty(entity, "Borrower")); + var loan_state = awi_demo.get_loan_state_string(getProperties(entity)); + ret["text"] = "Borrowed by " + this.get_person_str(borrower[0]) + " (" + loan_state.replace("_", " ") + ")"; + } else if (par.name === "SubSample" || par.name === "BagMean" || awi_references.isStripe(par)) { + ret = await awi_references.get_bag_and_icecore(entity); + ret["callback"] = awi_references.summarize_subsamples; + } else if (par.name === "Bag") { + ret = await awi_references.get_icecore(entity); + ret["callback"] = awi_references.summarize_box_content; + } else if (par.name === "Box") { + ret["text"] = getProperty(entity, "Number"); + } else if (par.name === "Palette") { + ret["text"] = getProperty(entity, "Number"); } else if (par.name === "Referenced") { + ret = this.get_referenced(entity); + } else { + var name = getEntityName(entity); + if (typeof name !== "undefined" && name.length > 0) { + ret["text"] = name; } } - return undefined; - } - const _stripe_re = /Stripe$/i; - this.isStripe = function(el) { - return _stripe_re.test(el.name) + return ret; } - /* - * Function that retrieves meaningful information for a single element. + this._target_class = "caosdb-resolve-reference-target"; + + /** + * Add a target span where the resolved reference information can be shown. * - * This function needs to be customized for specific implementations. + * If the element has a target yet, the existing one is returned. * - * rs: Element having the class caosdb-resolvable-reference and including a caosdb-resolve-reference target. + * @param {HTMLElement} element - where to append the target. + * @return {HTMLElement} the new/existing target element. */ - this.update_single_resolvable_reference = async function (rs) { - // remove caosdb-resolvable-reference class because this reference is - // being resolved right now. - $(rs).toggleClass("caosdb-resolvable-reference", false); - - var rseditable = rs.getElementsByClassName("caosdb-resolve-reference-target")[0]; - var id = getIDfromHREF(rs); - rseditable.textContent = id; - var el = await retrieve(getIDfromHREF(rs)); - var pr = getParents(el[0]); - if (getEntityHeadingAttribute(el[0], "path") !== undefined || pr[0].name == "Image") { - var pths = getEntityHeadingAttribute(el[0], "path").split("/"); - rseditable.textContent = pths[pths.length - 1]; - } else if (pr[0].name === "Person") { - rseditable.textContent = this.get_person_str(el[0]); - } else if (pr[0].name === "ExperimentSeries") { - rseditable.textContent = getEntityName(el[0]); - } else if (pr[0].name === "BoxType") { - rseditable.textContent = getEntityName(el[0]); - } else if (pr[0].name === "Loan") { - var persel = await retrieve(getProperty(el[0], "Borrower")); - var loan_state = awi_demo.get_loan_state_string(getProperties(el[0])); - rseditable.textContent = "Borrowed by " + this.get_person_str(persel[0]) + " (" + loan_state.replace("_", " ") + ")"; - } else if (pr[0].name === "SubSample" || this.isStripe(pr[0])) { - var bag = await this.find_bag_of_sample(el[0]); - if (!bag) { - var icecore = await this.find_ice_core_of_sample(el[0]); - if (!icecore) { - rseditable.textContent = `${id} (Sample w/o Bag or Ice Core)`; - } else { - rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, no Bag)`; - } - } else { - var icecore = (await query("SELECT name FROM IceCore WHICH REFERENCES " + getEntityID(bag)))[0]; - if (!icecore) { - rseditable.textContent = `${id} (Bag ${getProperty(bag, "Number", false)}, no Ice Core)`; - } else { - rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, Bag ${getProperty(bag, "Number", false)})`; - } - } - } else if (pr[0].name === "Bag") { - var bag = el[0]; - var icecore = (await query("SELECT name FROM IceCore WHICH REFERENCES " + getEntityID(bag)))[0]; - if (!icecore) { - rseditable.textContent = `${id} (Number ${getProperty(bag, "Number", false)}, no Ice Core)`; - } else { - rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, Number ${getProperty(bag, "Number", false)})`; - } - } else if (pr[0].name === "Box") { - rseditable.textContent = getProperty(el[0], "Number"); - } else if (pr[0].name === "Palette") { - rseditable.textContent = getProperty(el[0], "Number"); + this.add_target = function (element) { + if(element.getElementsByClassName(this._target_class).length > 0){ + return element.getElementsByClassName(this._target_class); } else { - if (typeof el[0].name !== "undefined" && el[0].length > 0) { - rseditable.textContent = el[0].name; - } + return $(`<span class="${this._target_class}"/>`) + .appendTo(element)[0]; } + } + /** + * Retrieve meaningful information for a single caosdb-f-reference-value + * element. + * + * @param {HTMLElement} rs - resolvable reference link + * @return {reference_info} the resolved reference information + */ + this.update_single_resolvable_reference = async function (rs) { + const target = this.add_target(rs); + const id = getIDfromHREF(rs); + target.textContent = id; + const resolved_entity_info = await this.resolve_reference(id); + target.textContent = resolved_entity_info.text; + return resolved_entity_info; + } + + + /** + * Add a summary field to the the list_values element. + * + * A summary field is a DIV element with class + * `caosdb-resolve-reference-summary`. The summary field is used to display + * a condensed string representation of the referenced entities in the + * list-property. + * + * @param {HTMLElement} list_values - where to add the summary field. + * @return {HTMLElement} a summary field. + */ + this.add_summary_field = function (list_values) { + const summary = $( + '<div class="caosdb-resolve-reference-summary"/>'); + $(list_values).prepend(summary); + return summary[0]; + } + + this._unresolved_class_name = "caosdb-resolvable-reference"; + + this.get_resolvable_properties = function (container) { + const _magic_class_name = this._unresolved_class_name; + return $(container).find(".caosdb-property-value").has( + `.${_magic_class_name}`).toArray(); } /* - * This function updates all references that are inside of the current viewport. + * This function updates all references in the body which are inside of the + * current viewport. + * + * If the optional container parameter is given, only elements inside the + * container are being processed. * + * @param {HTMLElement} container + * @return {Promise[]} array of promises for ref_infos. */ - this.update_visible_references = async function () { - var rs = $(".caosdb-resolvable-reference"); + this.update_visible_references = function (container) { + const property_values = resolve_references + .get_resolvable_properties(container || document.body); + + const _magic_class_name = resolve_references + ._unresolved_class_name; - for (var i = 0; i < rs.length; i++) { - if (is_in_viewport_vertically(rs[i]) && - is_in_viewport_horizontally(rs[i])) { - this.update_single_resolvable_reference(rs[i]); + var all_ref_infos = []; + for (const property_value of property_values) { + const lists = $(property_value).find( + ".caosdb-value-list").has( + `.${_magic_class_name}`); + + if (lists.length > 0) { + const summary_field = resolve_references + .add_summary_field(property_value); + + logger.debug("processing lists of references", lists); + for (var i = 0; i < lists.length; i++) { + const list = lists[i]; + if (resolve_references + .is_in_viewport_vertically(list)) { + const rs = $(list).find( + `.${_magic_class_name}`) + .toggleClass(_magic_class_name, false); + const ref_infos = []; + for (var j = 0; j < rs.length; j++) { + const ref_info = resolve_references + .update_single_resolvable_reference(rs[j]); + ref_info["index"] = j; + ref_infos.push(ref_info); + } + + // wait for resolution of references, + // then generate the summary. + Promise.all(ref_infos) + .then(_ref_infos => reference_list_summary + .generate(_ref_infos, summary_field)) + .catch(logger.error); + + all_ref_infos = all_ref_infos.concat(ref_infos); + } + } + } else { + // TODO merge code with the above? + const rs = $(property_value).find( + `.${_magic_class_name}`); + logger.debug("processing single references", rs); + for (var i = 0; i < rs.length; i++) { + if (resolve_references.is_in_viewport_vertically( + rs[i]) && + resolve_references.is_in_viewport_horizontally( + rs[i])) { + $(rs).toggleClass(_magic_class_name, false); + all_ref_infos.push(resolve_references + .update_single_resolvable_reference(rs[i])); + } + } } } + + return all_ref_infos; } } $(document).ready(function () { - resolve_references.init(); - var scrollTimeout = undefined; - var updatefunc = () => { - if (scrollTimeout) { - clearTimeout(scrollTimeout); - } - scrollTimeout = setTimeout(function () { - resolve_references.update_visible_references(); - }, 500); - }; - $(window).scroll(updatefunc); - $(".caosdb-value-list").scroll(updatefunc); + caosdb_modules.register(resolve_references); }); diff --git a/src/core/js/ext_xls_download.js b/src/core/js/ext_xls_download.js index 37349b35a609c57b3cddd371b3b37ebc9a93fdb2..d8953840c580fa742ef35f6fdd3981748c1e0d35 100644 --- a/src/core/js/ext_xls_download.js +++ b/src/core/js/ext_xls_download.js @@ -2,7 +2,8 @@ * ** header v3.0 * This file is a part of the CaosDB Project. * - * Copyright (C) 2019 IndiScale GmbH + * Copyright (C) 2019 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2019 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 @@ -22,28 +23,263 @@ 'use strict'; /** - * Functions which depend on server-side executable scripts. + * @module caosdb_table_export + * @version 0.1 * - * Dependency: webcaosdb + * Convert Entities for TSV and XLS export. + * + * The XLS part depends on a server-side executable script. * + * Dependency: webcaosdb */ +var caosdb_table_export = new function () { + + var logger = log.getLogger("caosdb_table_export"); + var TAB = "%09"; + var NEWLINE = "%0A"; + + /** + * Hide "Download XLS File" link if the user is not authenticted (i.e. + * has the `anonymous` role). + */ + this.init = function() { + logger.info("init caosdb_table_export"); + // TODO with AMD, use userIsAnonymous() + if (Array.from( + document.getElementsByClassName("caosdb-user-role")).map( + el => el.innerText + ).filter(el => el == "anonymous").length > 0) { + $(".caosdb-v-query-select-data-xsl").parent().hide(); + } + } + + + /** + * Convert the entities of the select-table to a tsv string. + * + * @param {boolean} raw - if true, the raw entity ids are put into the + * cells. Otherwise, the displayed data is used instead. + * @return {string} + */ + this.get_tsv_string = function(raw) { + const table = $('.caosdb-select-table'); + const columns = table.find("th").toArray() + .map(e => e.textContent) + .filter(e => e.length > 0); + // TODO use entity-panel class in table as well (change in query.xsl + // and then here) + const entities = table.find("tbody tr").toArray(); + const csv_string = this._get_tsv_string(entities, columns, raw); + return csv_string; + } + + /** + * Convert all entities to a tsv string with the given columns. + * + * @param {HTMLElement[]} entities - entities which are converted to rows + * of the tsv string. + * @param {string[]} columns - array of property names. + * @param {boolean} raw - if true, the raw entity ids are put into the + * cells. Otherwise, the displayed data is used instead. + * @return {string} + */ + this._get_tsv_string = function (entities, columns, raw) { + logger.trace("enter get_tsv_string", entities, columns); + var preamble = "data:text/csv;charset=utf-8,"; + var header = "ID" + TAB + columns.join(TAB) + NEWLINE + var rows = caosdb_table_export._get_tsv_rows(entities, columns, raw).join(NEWLINE); + + const ret = `${preamble}${header}${rows}`; + logger.trace("leave get_tsv_string", ret); + return ret; + } + + /** + * Return an array of rows with the given columns of the tsv table, one per + * entity. + * + * @param {HTMLElement[]} entities - entities which are converted to rows + * of the tsv string. + * @param {string[]} columns - array of property names. + * @param {boolean} raw - if true, the raw entity ids are put into the + * cells. Otherwise, the displayed data is used instead. + * @return {string[]} + */ + this._get_tsv_rows = function (entities, columns, raw) { + var rows = []; + for (const entity of entities) { + rows.push(this._get_entity_row(entity, columns, raw)); + } + return rows; + } + + + /** + * Return different string representations of the property's value. + * + * Returns an object with three string properties: raw, pretty, summary + * + * `raw` is just the raw property value string representation which is + * returned by the server. In case of list properties, this is a + * comma-separated list of the raw strings. + * + * `pretty` is used only for references and lists of references. It is the + * string which is generated by the ext-references module as a replacement + * for the entity id. If there is no such replacement, `pretty` is + * undefined. + * + * `summary` is used only for lists of references. It is the string summary + * of lists of references which is generated by the ext-references module. + * If there is no such replacement, `summary` is undefined. + * + * @param {HTMLElement} property + * @return {object} + */ + this._get_property_value = function(property) { + const value_element = $(property) + .find(".caosdb-property-value") + .first(); + const raw_value = value_element + .find(".caosdb-f-property-single-raw-value") + .toArray(); + const pretty_value = value_element + .find(".caosdb-resolve-reference-target") + .toArray(); + var summary_value = value_element + .find(".caosdb-resolve-reference-summary") + .find(".caosdb-f-resolve-reference-summary-plain") + .toArray(); + if (summary_value.length === 0) { + summary_value = value_element + .find(".caosdb-resolve-reference-summary").toArray(); + } + + return { + "raw": caosdb_table_export._to_string_value(raw_value), + "pretty": caosdb_table_export._to_string_value(pretty_value), + "summary": caosdb_table_export._to_string_value(summary_value), + }; + } + + /** + * Convert an array of property value elements to string. + * + * Empty arrays result in an emtpy string. + * + * One-element-arrays result in the text content of the element. + * + * N-element-arrays result in the a comma-separated list of the text + * content of the elements. + * + * @param {HTMLElement[]} value_elements + * @return {string} + */ + this._to_string_value = function(value_elements) { + if (value_elements.length === 0) { + return ""; + } else if (value_elements.length === 1) { + return $(value_elements[0]) + .text(); + } else { + return value_elements + .map(e => $(e).text()) + .join(", "); + } + } -function _get_csv_string(){ - const csv_string = document.getElementById("caosdb-f-query-select-data-tsv").getAttribute( - "href"); - if (!csv_string) { - return undefined; + + /** + * Return an array of cells, one per column, which contain a string + * representation of the value of the properties with the same name (as the + * column). + * + * @param {HTMLElement} entity - entity from which the cells are extracted. + * @param {string[]} columns - array of property names. + * @param {boolean} raw - if true, the raw entity ids are put into the + * cells. Otherwise, the displayed data is used instead. + * @return {string[]} + */ + this._get_entity_row = function (entity, columns, raw) { + var cells = [getEntityID(entity)]; + var properties = getProperties(entity); + + for (const column of columns) { + var cell = ""; + for (const property of properties) { + if(property.name.toLowerCase() === column.toLowerCase()) { + var value = caosdb_table_export + ._get_property_value(property.html); + if (raw) { + cell = value.raw; + } else if (value.summary) { + cell = value.summary; + } else if (value.pretty) { + cell = value.pretty; + } else { + cell = value.raw; + } + } + } + cells.push(cell); + } + + logger.trace("leave _get_entity_row", cells); + return cells.join(TAB); + } + + + /** + * Open the resulting xls file by setting href to the location of the resulting + * file in the server's `Shared` resource and imitate a click. + */ + this._go_to_script_results = function (xls_link, filename) { + xls_link.setAttribute( + "href", + location.protocol + "//" +location.host + "/Shared/" + filename); + xls_link.click(); + } + + + this._get_csv_string = function (){ + const raw = $("input#caosdb-table-export-raw-flag-xls").is(":checked"); + const csv_string = caosdb_table_export.get_tsv_string(raw); + //const csv_string = document.getElementById("caosdb-f-query-select-data-tsv").getAttribute( + //"href"); + if (!csv_string) { + return undefined; + } + + return decodeURIComponent(csv_string.replace(/^data.*utf-8,/, "")); } +} + + +/** + * This function is called on click by the link button which says "Download TSV + * File". + * + * It sets the href attribute of the link to a string which gerenates a + * downloadable file. All entities of the select-table are included in the + * resulting file. + */ +function downloadTSV(tsv_link) { + const raw = $("input#caosdb-table-export-raw-flag-tsv").is(":checked"); + const tsv_string = caosdb_table_export.get_tsv_string(raw); - return decodeURIComponent(csv_string.replace(/^data.*utf-8,/, "")); + $(tsv_link).attr("href", tsv_string); + return true; } /** - * Call the server-side script `xls_from_csv.py` and generate a XLS file from - * TSV string content. + * This function is called on click by the link button which says "Download XLS + * File". + * + * It calls the server-side script `xls_from_csv.py` and generate a XLS file + * from TSV string content. All entities of the select-table are included in the + * resulting file. */ async function downloadXLS(xls_link) { - const csv_string = _get_csv_string(); + const csv_string = caosdb_table_export._get_csv_string(); // remove click event handler which called this function in the first place const onClickValue = xls_link.getAttribute("onClick"); @@ -64,18 +300,18 @@ async function downloadXLS(xls_link) { } // set the href in order to download the file and simulate a click. - _go_to_script_results(xls_link, filename); + caosdb_table_export._go_to_script_results(xls_link, filename); } catch (e) { globalError(e); - // restore the old click handler + } finally { + // restore the old click handler - hence a new file is generated with each click. xls_link.setAttribute("onClick", onClickValue); - }; + } + return true; } -function _go_to_script_results(xls_link, filename) { - xls_link.setAttribute( - "href", - location.protocol + "//" +location.host + "/Shared/" + filename); - xls_link.click(); -} + +$(document).ready(function () { + caosdb_modules.register(caosdb_table_export); +}); diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index 3c4b195410abc8b58571c99bd4b1673f46368594..28226e80abe4bd392563e03d2d301c3589b669d8 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -963,7 +963,7 @@ var form_elements = new function () { */ this._make_field_wrapper = function (name) { caosdb_utils.assert_string(name, "param `name`"); - return $('<div class="form-group caosdb-f-field caosdb-property-row" data-field-name="' + name + '" />') + return $('<div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="' + name + '" />') .css({"padding": "0"})[0]; } diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index c213a0ebcd766a1d1c2502cc8aaae2fc014ff840..56f00429e63b8454a88c952e39bb5666a57ca583 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -76,6 +76,7 @@ this.navbar = new function() { } $(button_elem).toggleClass("navbar-btn", true); $(button_elem).toggleClass("btn", true); + $(button_elem).toggleClass("btn-link", true); // bind click if(typeof click_callback === "function") { @@ -331,6 +332,7 @@ this.transformation = new function() { * Transform the server's response with multiple entities into their * html representation. The parameter `xml` may also be a Promise. * + * @async * @param {XMLDocument} xml * @return {HTMLElement[]} an array of HTMLElements. */ @@ -384,9 +386,14 @@ this.transformation = new function() { * Retrieve the entity.xsl script and modify it such that we can use it * without the context of the main.xsl. * + * @param {string} [root_template] a string to be injected as a root + * template. Optional. If none is given a default template is injected + * which calls the entity templates which perform the transformation of + * xml entities to the canocical HTML representation. * @return {XMLDocument} xslt script */ - this.retrieveEntityXsl = async function _rEX() { + this.retrieveEntityXsl = async function _rEX(root_template) { + const _root = root_template || '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>'; var entityXsl = await transformation.retrieveXsltScript("entity.xsl"); var commonXsl = await transformation.retrieveXsltScript("common.xsl"); var errorXsl = await transformation.retrieveXsltScript('messages.xsl'); @@ -394,7 +401,7 @@ this.transformation = new function() { insertParam(xslt, "filesystempath", connection.getBasePath() + "FileSystem/"); insertParam(xslt, "entitypath", connection.getBasePath() + "Entity/"); insertParam(xslt, "close-char", '×'); - xslt = injectTemplate(xslt, '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>'); + xslt = injectTemplate(xslt, _root); return xslt; } @@ -1083,29 +1090,65 @@ var hintMessages = new function() { $(entity).find(".alert").show(); } + /** + * Replace all (too-big) message divs with (tiny) badges. + * + * When an entity has many errors, warnings and info messages they will + * likely litter up the entity panel (and the property rows). This function + * replaces all message divs with tiny, clickable badges showing just the + * message type (error, warning, info). On click they are replaced with the + * original message div again. + * + * This method can be called on an entity panel, but also on any element + * which contains message divs at any depth in the DOM tree. + * + * @param {HTMLElement} entity - the element where to replace the + * messages. + */ this.hintMessages = function(entity) { + + // TODO refactor such that the function can detect whether a message is + // replaced yet instead of "unhintMessage"ing all of them first and do + // all the work again. this.unhintMessages(entity); - var messageType = { + + // dictionary for mapping bootstrap classes to message types + const messageType = { "info": "info", "warning": "warning", "danger": "error" }; for (let alrt in messageType) { + + // find all message divs $(entity).find(".alert.alert-" + alrt).each(function(index) { var messageElem = $(this); + + // this way only one badge is shown, even if there are more + // than one message of the given type. if (messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).length == 0) { - messageElem.parent('.caosdb-messages, .caosdb-property-row').prepend('<button title="Click here to show the ' + messageType[alrt] + ' messages of the last transaction." class="btn caosdb-f-message-badge badge alert-' + alrt + '">' + messageType[alrt] + '</button>'); + + // TODO why is the message badge added to the .caosdb-v-property-row here? shouldn't .caosdb-messages suffice? + messageElem.parent('.caosdb-messages, .caosdb-v-property-row').prepend('<button title="Click here to show the ' + messageType[alrt] + ' messages of the last transaction." class="btn caosdb-v-message-badge caosdb-f-message-badge badge alert-' + alrt + '">' + messageType[alrt] + '</button>'); messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).on("click", function(e) { + + // TODO use remove here instead of hide? $(this).hide(); - console.log(this); - console.log(alrt); + + // TODO use messageElem.show() here? or even append + // here and remove in the next statement? messageElem.parent().find('.alert.alert-' + alrt).show() }); + } else { + // badge found + // TODO add counter to the badge for each message type } + messageElem.hide(); }); } + // moves all badges into one div with text-right if ($(entity).find(".caosdb-messages > .caosdb-f-message-badge").length > 0) { var div = $('<div class="text-right" style="padding: 5px 16px;"/>'); div.prependTo($(entity).find(".caosdb-messages")); @@ -1113,7 +1156,9 @@ var hintMessages = new function() { messageBadges.detach(); div.append(messageBadges); } - $(entity).find(".caosdb-property-row > .caosdb-f-message-badge").addClass("pull-right"); + + // see the other TODO above: why is there a .caosdb-v-message-badge inside the caosdb-v-property-row without a div.caosdb-message + $(entity).find(".caosdb-v-property-row > .caosdb-v-message-badge").addClass("pull-right"); } } diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index 621755604a87b9bb7fbb9d8125ce414a11757d32..26f0c11677e328c22eb70440772b3cb53bc4c146 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -232,7 +232,7 @@ </xsl:template> <!-- PROPERTIES --> <xsl:template match="Property" mode="entity-body"> - <li class="list-group-item caosdb-property-row"> + <li class="list-group-item caosdb-v-property-row caosdb-f-entity-property"> <xsl:attribute name="id"> <xsl:value-of select="generate-id()"/> </xsl:attribute> @@ -304,42 +304,46 @@ <xsl:choose> <xsl:when test="$reference='true' and normalize-space($value)!=''"> <!-- this is a reference --> - <a class="btn btn-default btn-sm caosdb-resolvable-reference"> + <a class="btn btn-default btn-sm caosdb-f-reference-value caosdb-resolvable-reference"> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath,normalize-space($value))"/> </xsl:attribute> - <span class="caosdb-id caosdb-id-button"> + <xsl:element name="span"> + <xsl:attribute name="class"> + <xsl:value-of select="'caosdb-f-property-single-raw-value caosdb-id caosdb-id-button'"/> + </xsl:attribute> <xsl:value-of select="normalize-space($value)"/> - </span> - <span class="caosdb-resolve-reference-target"/> + </xsl:element> </a> </xsl:when> <xsl:when test="$boolean='true'"> <xsl:element name="span"> <xsl:attribute name="class"> - <xsl:value-of select="concat('caosdb-boolean-',normalize-space($value)='TRUE')"/> + <xsl:value-of select="concat('caosdb-f-property-single-raw-value caosdb-boolean-',normalize-space($value)='TRUE')"/> </xsl:attribute> <xsl:value-of select="normalize-space($value)"/> </xsl:element> </xsl:when> <xsl:otherwise> - <span class="caosdb-property-text-value"> + <xsl:element name="span"> + <xsl:attribute name="class"> + <xsl:value-of select="'caosdb-f-property-single-raw-value caosdb-property-text-value'"/> + </xsl:attribute> <xsl:call-template name="trim"> <xsl:with-param name="str"> <xsl:value-of select="$value"/> </xsl:with-param> </xsl:call-template> - </span> + </xsl:element> </xsl:otherwise> </xsl:choose> </xsl:when> <xsl:otherwise> - <span class="caosdb-property-text-value"/> + <span class="caosdb-f-property-single-raw-value caosdb-property-text-value"/> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="Property" mode="property-reference-value-list"> - <xsl:param name="reference"/> <div class="caosdb-value-list"> <xsl:element name="div"> <xsl:attribute name="class">btn-group btn-group-sm caosdb-overflow-content</xsl:attribute> diff --git a/src/core/xsl/entity_palette.xsl b/src/core/xsl/entity_palette.xsl index bbe3a7dc7e5c2dff2cd0442a0e5702434a6d6e83..aea42a8750a74f509557bcf842e4d1f8dc97bef9 100644 --- a/src/core/xsl/entity_palette.xsl +++ b/src/core/xsl/entity_palette.xsl @@ -3,11 +3,11 @@ <xsl:output method="html"/> <xsl:template match="/Response"> - <div class="btn-group-vertical"> + <div class="btn-group-vertical caosdb-v-editmode-btngroup"> <button type="button" class="btn btn-default caosdb-f-edit-panel-new-button new-property">Create new Property</button> <button type="button" class="btn btn-default caosdb-f-edit-panel-new-button new-recordtype">Create new RecordType</button> </div> - <div title="Drag and drop Properties from this panel to the Entities on the left." class="panel panel-default"> + <div title="Drag and drop Properties from this panel to the Entities on the left." class="panel panel-default caosdb-v-editmode-existing"> <div class="panel-heading"> <h5>Existing Properties</h5> </div> @@ -23,7 +23,7 @@ </ul> </div> </div> - <div title="Drag and drop RecordTypes from this panel to the Entities on the left." class="panel panel-default"> + <div title="Drag and drop RecordTypes from this panel to the Entities on the left." class="panel panel-default caosdb-v-editmode-existing"> <div class="panel-heading"> <h5>Existing RecordTypes</h5> </div> diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl index a0b299e7668ca87153761396f7b93a61e9169978..3f4b268cb8c210012567196d1a6b2e25b2259940 100644 --- a/src/core/xsl/navbar.xsl +++ b/src/core/xsl/navbar.xsl @@ -150,6 +150,7 @@ <xsl:with-param name="class" select="'alert-info'"/> </xsl:apply-templates> </nav> + <div class="container" id="subnav"/> </xsl:template> <xsl:template match="Role" name="caosdb-user-roles"> <div class="caosdb-user-role"> diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl index 7aa9a80cf425767b9d49d9ed52170618f66317f7..0015fe8d5beef5f1fc67c894b5b1421e2aaab4bb 100644 --- a/src/core/xsl/query.xsl +++ b/src/core/xsl/query.xsl @@ -101,16 +101,16 @@ </div> <div class="modal-body"> <p> - <a id="caosdb-f-query-select-data-tsv" download="selected_data.tsv"> - <xsl:attribute name="href"> - <xsl:value-of select="'data:text/csv;charset=utf-8,'"/><xsl:for-each select="Selector"><xsl:value-of select="@name"/><xsl:if test="position()!=last()"><xsl:value-of select="'%09'"/></xsl:if></xsl:for-each><xsl:for-each select="/Response/*[@id]"><xsl:call-template name="select-table-row-plain"><xsl:with-param name="entity-id" select="@id"/></xsl:call-template></xsl:for-each></xsl:attribute> + <a id="caosdb-f-query-select-data-tsv" onclick="downloadTSV(this)" href="#selected_data.tsv" download="selected_data.tsv"> Download TSV File </a> + <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-tsv" title="Export raw entity ids instead of the visible page content."/>raw</label></span> </p> <p> - <a class="caosdb-v-query-select-data-xsl" onclick="downloadXLS(this)" href="#selected_data.xsl" download=""> - Download XLS File + <a class="caosdb-v-query-select-data-xsl" onclick="downloadXLS(this)" href="#selected_data.xsl" download=""> + Download XLS File </a> + <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-xls" title="Export raw entity ids instead of the visible page content."/>raw</label></span> </p> <hr/> <p> @@ -172,6 +172,9 @@ <xsl:template name="select-table-row"> <xsl:param name="entity-id"/> <tr> + <xsl:attribute name="data-entity-id"> + <xsl:value-of select="$entity-id"/> + </xsl:attribute> <td> <xsl:call-template name="entity-link"> <xsl:with-param name="entity-id" select="$entity-id"/> @@ -188,8 +191,11 @@ <xsl:template name="select-table-cell"> <xsl:param name="entity-id"/> <xsl:param name="field-name"/> - <td> - <div class="caosdb-v-property-value"> + <td class="caosdb-f-entity-property"> + <xsl:attribute name="data-property-name"> + <xsl:value-of select="$field-name"/> + </xsl:attribute> + <div class="caosdb-property-value caosdb-v-property-value"> <xsl:choose> <xsl:when test="/Response/*[@id=$entity-id]/@*[translate(name(),$uppercase, $lowercase)=$field-name]"> <xsl:value-of select="/Response/*[@id=$entity-id]/@*[translate(name(), $uppercase, $lowercase)=$field-name]"/> @@ -201,23 +207,6 @@ </div> </td> </xsl:template> - <!-- For CSV download --> - <!-- This block is responsible for generating the downloadable TSV. --> - <xsl:template name="select-table-row-plain"> - <xsl:param name="entity-id"/> - <xsl:value-of select="'%0A'"/><xsl:for-each select="/Response/Query/Selection/Selector"><xsl:call-template name="select-table-cell-plain"><xsl:with-param name="entity-id" select="$entity-id"/><xsl:with-param name="field-name" select="translate(@name, $uppercase, $lowercase)"/></xsl:call-template><xsl:if test="position()!=last()"><xsl:value-of select="'%09'"/></xsl:if></xsl:for-each></xsl:template> - <xsl:template name="select-table-cell-plain"> - <xsl:param name="entity-id"/> - <xsl:param name="field-name"/> - <xsl:choose> - <xsl:when test="/Response/*[@id=$entity-id]/@*[translate(name(),$uppercase, $lowercase)=$field-name]"> - <xsl:value-of select="/Response/*[@id=$entity-id]/@*[translate(name(), $uppercase, $lowercase)=$field-name]"/> - </xsl:when> - <xsl:when test="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"> - <xsl:apply-templates mode="property-value-plain" select="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"></xsl:apply-templates> - </xsl:when> - </xsl:choose> - </xsl:template> <xsl:template name="caosdb-query-panel"> <div class="container caosdb-query-panel"> <form class="panel" id="caosdb-query-form" method="GET"> diff --git a/src/ext/js/fileupload.js b/src/ext/js/fileupload.js index 4d223116e7d771e56404076dbb2e64e48832b000..103af1914f2786ef6d8edd5987b04d6de215e2fc 100644 --- a/src/ext/js/fileupload.js +++ b/src/ext/js/fileupload.js @@ -233,7 +233,7 @@ var fileupload = new function() { // add global listener for start_edit event document.body.addEventListener(edit_mode.start_edit.type, function(e) { - $(e.target).find(".caosdb-properties .caosdb-property-row").each(function(idx) { + $(e.target).find(".caosdb-properties .caosdb-f-entity-property").each(function(idx) { fileupload.create_upload_app(this); }); }, true); diff --git a/test/core/html/form_elements_example_1.html b/test/core/html/form_elements_example_1.html index eacc71f5346b1f5469d84fc409ce21ed0aff459f..d2d6fd34cd8e11f139d87f1cad6263fe482d72a8 100644 --- a/test/core/html/form_elements_example_1.html +++ b/test/core/html/form_elements_example_1.html @@ -1,6 +1,6 @@ <div class="caosdb-f-form-wrapper"> <form action="#" class="form-horizontal" method="post" name="sample_creation.py"> - <div class="form-group caosdb-f-field caosdb-property-row caosdb-f-form-field-required caosdb-f-form-field-cached" data-field-name="ice_core" data-groups="(part1)"> + <div class="form-group caosdb-f-field caosdb-f-entity-property caosdb-f-form-field-required caosdb-f-form-field-cached" data-field-name="ice_core" data-groups="(part1)"> <label class="control-label col-sm-3" data-property-name="ice_core" for="ice_core">Ice Core</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> @@ -61,7 +61,7 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="orig_sample_type" data-groups="(part1)"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="orig_sample_type" data-groups="(part1)"> <label class="control-label col-sm-3" data-property-name="orig_sample_type" for="orig_sample_type">Original Sample Type</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> @@ -128,7 +128,7 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row caosdb-f-field-disabled" data-field-name="logging_protocol" data-groups="(part2)" style="display: none;"> + <div class="form-group caosdb-f-field caosdb-f-entity-property caosdb-f-field-disabled" data-field-name="logging_protocol" data-groups="(part2)" style="display: none;"> <label class="control-label col-sm-3" data-property-name="logging_protocol" for="logging_protocol">Logging Protocol</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> @@ -154,7 +154,7 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="cutting_protocol" data-groups="(part3)" style=""> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="cutting_protocol" data-groups="(part3)" style=""> <label class="control-label col-sm-3" data-property-name="cutting_protocol" for="cutting_protocol">Cutting Protocol</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> @@ -191,28 +191,28 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row caosdb-f-form-field-required" data-field-name="cutting_date" data-groups="(part2)(part3)" style=""> + <div class="form-group caosdb-f-field caosdb-f-entity-property caosdb-f-form-field-required" data-field-name="cutting_date" data-groups="(part2)(part3)" style=""> <label class="control-label col-sm-3" data-property-name="cutting_date" for="cutting_date">Cutting Date</label> <div class="caosdb-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="cutting_date" type="date"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="bag_numbers" data-groups="(part2)(part3)" style=""> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers" data-groups="(part2)(part3)" style=""> <label class="control-label col-sm-3" data-property-name="bag_numbers" for="bag_numbers">Bag Numbers</label> - <div class="caosdb-f-field caosdb-property-row" data-field-name="bag_numbers_from"> + <div class="caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers_from"> <label class="control-label col-sm-1" data-property-name="bag_numbers_from" for="bag_numbers_from">from</label> <div class="caosdb-property-value col-sm-3"> <input class="form-control caosdb-property-text-value" name="bag_numbers_from" step="1" type="number"/> </div> </div> - <div class="caosdb-f-field caosdb-property-row" data-field-name="bag_numbers_to"> + <div class="caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers_to"> <label class="control-label col-sm-1 col-sm-offset-1" data-property-name="bag_numbers_to" for="bag_numbers_to">to</label> <div class="caosdb-property-value col-sm-3"> <input class="form-control caosdb-property-text-value" name="bag_numbers_to" step="1" type="number"/> </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="box_of_new_samples" data-groups="(part2)(part3)" style=""> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="box_of_new_samples" data-groups="(part2)(part3)" style=""> <label class="control-label col-sm-3" data-property-name="box_of_new_samples" for="box_of_new_samples">Box of New Samples</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> @@ -285,7 +285,7 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="new_subsamples_selector" data-groups="(part3)" style=""> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="new_subsamples_selector" data-groups="(part3)" style=""> <label class="control-label col-sm-3" data-property-name="new_subsamples_selector" for="new_subsamples_selector">New Subsamples</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select show-tick form-control bs3"> @@ -333,24 +333,24 @@ <div class="col-sm-9 col-sm-offset-3 row" style="background-color: rgb(255, 255, 255);"> <fieldset class="form-inline caosdb-f-form-elements-subform" data-subform-name="new_subsamples" id="new_subsamples_6335" style="padding-left: 15px; padding-right: 15px;"> <legend>Subsample</legend> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="type"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="type"> <div class="caosdb-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="type" type="hidden" value="6335"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="width" for="width">width (cm)</label> <div class="caosdb-property-value"> <input class="form-control caosdb-property-text-value" name="width" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="height" for="height">height (cm)</label> <div class="caosdb-property-value"> <input class="form-control caosdb-property-text-value" name="height" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="rectangular" for="rectangular">rectangular</label> <div class="caosdb-property-value"> <input class="caosdb-property-text-value" name="rectangular" type="checkbox"/> @@ -359,24 +359,24 @@ </fieldset> <fieldset class="form-inline caosdb-f-form-elements-subform" data-subform-name="new_subsamples" id="new_subsamples_6338" style="padding-left: 15px; padding-right: 15px;"> <legend>PP_Sample</legend> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="type"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="type"> <div class="caosdb-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="type" type="hidden" value="6338"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="width" for="width">width (cm)</label> <div class="caosdb-property-value"> <input class="form-control caosdb-property-text-value" name="width" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="height" for="height">height (cm)</label> <div class="caosdb-property-value"> <input class="form-control caosdb-property-text-value" name="height" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-property-row" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> + <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="rectangular" for="rectangular">rectangular</label> <div class="caosdb-property-value"> <input class="caosdb-property-text-value" name="rectangular" type="checkbox"/> diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index 98fd3b2f9aac4f23f52edc6a57d6740d41600914..1ec57f31d7da949e6310958ab4516206e073d811 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -258,7 +258,7 @@ QUnit.test("make_property_editable", function(assert) { // test for correct parsing of datatypes var testEntity = this.testEntity_make_property_editable_1; - for (var element of $(testEntity).find('.caosdb-property-row')) { + for (var element of $(testEntity).find('.caosdb-f-entity-property')) { edit_mode.make_property_editable(element); } diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js index b6d461ce9461ca8d9dd2f9d82c5b580a268aa7c9..a853c36556c5482e0bd0b1f751755e88b871d25e 100644 --- a/test/core/js/modules/entity.xsl.js +++ b/test/core/js/modules/entity.xsl.js @@ -187,7 +187,7 @@ QUnit.test("single-value template with reference property.", function(assert) { 'value': '', 'reference': 'true', 'boolean': 'false' - })), "<span xmlns=\"http://www.w3.org/1999/xhtml\" class=\"caosdb-property-text-value\"></span>", "empty value produces empty span."); + })), "<span xmlns=\"http://www.w3.org/1999/xhtml\" class=\"caosdb-f-property-single-raw-value caosdb-property-text-value\"></span>", "empty value produces empty span."); let link = callTemplate(this.entityXSL, 'single-value', { 'value': '1234', 'reference': 'true', diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 88a66a1c11c6691d7f4db53a502a5734ab4cbb3f..972691364c5bfd402313e6b3ad8fd29ed572b4ed 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -30,12 +30,12 @@ QUnit.module("ext_map.js", { this.test_map_entity = ` <div class="caosdb-entity-panel caosdb-properties"> <div class="caosdb-id">1234</div> - <div class="list-group-item caosdb-property-row"> + <div class="list-group-item caosdb-f-entity-property"> <div class="caosdb-property-name">` + lat + `</div> <div class="caosdb-property-value">1.23</div> </div> - <div class="list-group-item caosdb-property-row"> + <div class="list-group-item caosdb-f-entity-property"> <div class="caosdb-property-name">` + lng + `</div> <div class="caosdb-property-value">5.23</div> diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js index 1789f31676590ff9293bdaa26cc3bd133f89824e..b45a3deb99000dd173c755f7d5f0c9e32f4e24f1 100644 --- a/test/core/js/modules/ext_references.js.js +++ b/test/core/js/modules/ext_references.js.js @@ -25,6 +25,23 @@ /* SETUP ext_references module */ QUnit.module("ext_references.js", { before: function(assert) { + // dummy test functions + var logger = log.getLogger("ext_references.js") + logger.setLevel("debug"); + + resolve_references.retrieve = async function (id) { + logger.debug("retrieve", id); + return transformation.transformEntities(str2xml( + `<Response> + <Record id="${id}" name="ReferenceObject-${id}"> + <Parent name="Referenced"/> + </Record> + </Response>` + )); + } + + resolve_references.is_in_viewport_horizontally = () => true; + resolve_references.is_in_viewport_vertically = () => true; } }); @@ -40,8 +57,43 @@ QUnit.test("get_person_str", function(assert){ assert.ok(resolve_references.get_person_str); }); -QUnit.test("update_single_resolvable_reference", function(assert){ - assert.ok(resolve_references.update_single_resolvable_reference); +QUnit.test("update_visible_references", async function(assert){ + const f = resolve_references.update_visible_references; + + const test_property = $(`<div class="caosdb-property-value"> + <div data-entity-id="15" + class="${resolve_references._unresolved_class_name}"> + <span class="${resolve_references._target_class}"/></span> + </div> + </div><div class="caosdb-property-value"> + <div class="caosdb-value-list"> + <div data-entity-id="16" + class="${resolve_references._unresolved_class_name}"> + <span class="${resolve_references._target_class}"/></span> + </div> + <div data-entity-id="17" + class="${resolve_references._unresolved_class_name}"> + <span class="${resolve_references._target_class}"/></span> + </div> + </div> + </div>`); + + assert.equal(test_property.find(`.${resolve_references._unresolved_class_name}`).length, 3, "is unresolved"); + + const ref_infos = f($("<div/>").append(test_property)); + + assert.equal(test_property.find(`.${resolve_references._unresolved_class_name}`).length, 0, "is resolved"); + + + assert.equal(ref_infos.length, 3, "three ref_infos"); + var done = assert.async(); + Promise.all(ref_infos).then(ref_infos => { + assert.equal(ref_infos[1].data.name.replace(/[0-9]*/g, ""), "ReferenceObject-", "has data"); + assert.ok(typeof ref_infos[1].callback === "function", + "has callback"); + done(); + }); + }); @@ -81,7 +133,62 @@ QUnit.test("check_structure_html", function(assert){ } // but the first element does NOT: assert.equal(elms[0].parentElement.parentElement.classList.contains("caosdb-value-list"), false); - + done(); }, err => {console.log(err);}); }); + +QUnit.module("reference_list_summary"); + +QUnit.test("simplify_integer_numbers", function(assert) { + var f = reference_list_summary.simplify_integer_numbers; + assert.equal(f([1]), "1"); + assert.equal(f([1,1,1]), "1"); + assert.equal(f([1,2]), "1, 2"); + assert.equal(f([2,1]), "1, 2"); + assert.equal(f([2,1,2]), "1, 2"); + assert.equal(f([2,1,2]), "1, 2"); + assert.equal(f([1,2,4]), "1-2, 4"); + assert.equal(f([1,3,4,5]), "1, 3-5"); + assert.equal(f([1,2,3,5,8,9,10]), "1-3, 5, 8-10"); + assert.equal(f([1,2,3,5,7,8,10]), "1-3, 5, 7-8, 10"); + assert.equal(f([1,3,5,7,8,10]), "1, 3, 5, 7-8, 10"); + assert.equal(f([1,2,4,5,7,8,10]), "1-2, 4-5, 7-8, 10"); + assert.equal(f([1,2,4,5,7,9,10]), "1-2, 4-5, 7, 9-10"); +}); + + +QUnit.test("generate", function(assert) { + const f = reference_list_summary.generate; + const summary_container = $("<div/>"); + + + // without callback function + const ref_infos_no_cb = [ { + "data": { "dummy": "dummy_data1" }, + }, { + "data": { "dummy": "dummy_data2" }, + }, ]; + + assert.ok(typeof f(ref_infos_no_cb) === "undefined", "no cb returns undefined"); + f(ref_infos_no_cb, summary_container); + assert.equal(summary_container.children().length, 0, "no cb doesn't append to summary container."); + assert.equal(summary_container.text(), "", "no text in container"); + + // with callback function + const ref_infos_with_cb = [ { + "callback": function (ref_infos) { + return ref_infos.map(i => i.data.dummy).join(","); + }, + "data": { "dummy": "dummy_data3" }, + }, { + "data": { "dummy": "dummy_data4" }, + }, ]; + + const summary = "dummy_data3,dummy_data4"; + + assert.equal(f(ref_infos_with_cb, summary_container), summary, "callback returns summary"); + assert.equal(summary_container.text(), summary, "summary in container"); + + +}); diff --git a/test/core/js/modules/ext_xls_download.js.js b/test/core/js/modules/ext_xls_download.js.js index 360b389725eec2bc6a4e4115e9cc46cd91e33d69..4f9dc6c59b156f1f2265acb4b315887536667194 100644 --- a/test/core/js/modules/ext_xls_download.js.js +++ b/test/core/js/modules/ext_xls_download.js.js @@ -21,7 +21,48 @@ */ 'use strict'; -QUnit.module("ext_xls_download"); +/** + * Transform the server's response of a select query request into the html + * tabular representation. The parameter `xml` may also be a Promise. + * + * Only used for testing purposes. + * + * @async + * @param {XMLDocument} xml + * @return {HTMLElement} DIV.caosdb-query-response + */ +transformation.transformSelectTable = async function _tST (xml) { + var root_template = '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/Query" mode="query-results"/></div></xsl:template>'; + var queryXsl = await transformation.retrieveXsltScript("query.xsl"); + var entityXsl = await transformation.retrieveEntityXsl(root_template); + insertParam(entityXsl, "uppercase", 'abcdefghijklmnopqrstuvwxyz'); + insertParam(entityXsl, "lowercase", 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); + var xsl = transformation.mergeXsltScripts(entityXsl, [queryXsl]); + var html = await asyncXslt(xml, xsl); + return $(html).find('div.root')[0]; +} + +QUnit.module("ext_xls_download", { + before: function (assert) { + var done = assert.async(); + var testCase = "table_export/test_case_select_table_1.xml"; + connection + .get("xml/"+ testCase) + .then(xml => { + return transformation. + //transformEntities(xml); + transformSelectTable(xml); + }).then(entities => { + this.test_case_1 = entities; + done(); + }).catch(err => { + console.error(err); + done(); + }); + + } +}); + { const sleep = function sleep(ms) { @@ -31,16 +72,18 @@ QUnit.module("ext_xls_download"); QUnit.test("call downloadXLS", async function(assert) { var done = assert.async(2); + // mock server response (successful) connection.runScript = async function(exec, param){ assert.equal(exec, "xls_from_csv.py", "call xls_from_csv.py"); done(); return str2xml('<response><script code="0" /><stdout>bla</stdout></response>'); } - _go_to_script_results = function(xls_link, filename) { + caosdb_table_export._go_to_script_results = function(xls_link, filename) { xls_link.setAttribute( "href", location.protocol + "//" +location.host + "/Shared/" + filename); + assert.equal(filename, "bla", "filename correct"); done(); } @@ -58,3 +101,61 @@ QUnit.module("ext_xls_download"); tsv_data.remove(); }); } + +QUnit.test("_get_property_value", function(assert) { + var f = caosdb_table_export._get_property_value; + + assert.equal(f().pretty, "", "No pretty content"); + assert.equal(f().raw, "", "No raw content"); + assert.equal(f().summary, "", "No summary"); +}); + + +QUnit.test("_get_tsv_string", function(assert) { + const table = this.test_case_1; + const entities = $(table).find("tbody tr").toArray(); + assert.equal(entities.length, 2, "two example entities"); + + var f = caosdb_table_export._get_tsv_string + var tsv_string = f(entities, ["Bag", "Number"], true); + assert.equal(tsv_string, "data:text/csv;charset=utf-8,ID%09Bag%09Number%0A242%096366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413%090284%0A2112%09%091101", "tsv generated"); +}); + +QUnit.test("_get_property_value", function (assert) { + const table = this.test_case_1; + const entity = $(table).find("tbody tr")[0]; + + const property = getProperties(entity)[0].html; + + var f = caosdb_table_export._get_property_value; + var ret = f(property); + assert.equal(ret.pretty, "", "pretty is empty"); + assert.equal(ret.summary, "", "summary is empty"); + assert.equal(ret.raw, "6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413", "raw contains ids"); + + var unresolved = property + .getElementsByClassName( + resolve_references + ._unresolved_class_name); + + for (const el of unresolved) { + var target = resolve_references + .add_target(el); + $(target).append("bla"); + } + + ret = f(property); + assert.equal(ret.pretty, "bla, bla, bla, bla, bla, bla, bla, bla, bla", "pretty is empty"); + assert.equal(ret.summary, "", "summary is empty"); + assert.equal(ret.raw, "6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413", "raw contains ids"); + + + var summary = resolve_references.add_summary_field(property); + $(summary).find(".coasdb-property-value").append("summary bla"); + + ret = f(property); + assert.equal(ret.pretty, "bla, bla, bla, bla, bla, bla, bla, bla, bla", "pretty shows list of reference info text"); + assert.equal(ret.summary, "", "summary is empty"); + assert.equal(ret.raw, "6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413", "raw contains ids"); + +}); diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 7e02d1d98270f0c5f696bcbe39651fe0b3064c03..874b68e137121ac886082212cda564ec53da3005 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -1860,4 +1860,15 @@ QUnit.test("annotation module", function(assert) { assert.ok(annotation.initNewCommentApp, "initNewCommentApp exists."); }); -/* MISC FUNCTIONS */ + +/* MODULE navbar */ +QUnit.module("webcaosdb.js - navbar", { +}); + +QUnit.test("test button classes", function(assert) { + var result = $(navbar.add_button("TestButton")).children().first() + assert.ok(result.hasClass("navbar-btn"), "has class navbar-btn"); + assert.ok(result.hasClass("btn"), "has class btn"); + assert.ok(result.hasClass("btn-link"), "has class btn-link"); + assert.equal(result.text(), "TestButton", "text is correct"); +}); diff --git a/test/core/xml/table_export/test_case_select_table_1.xml b/test/core/xml/table_export/test_case_select_table_1.xml new file mode 100644 index 0000000000000000000000000000000000000000..12f26180b89bdf3f2ba3280d4546783f54bfa8ea --- /dev/null +++ b/test/core/xml/table_export/test_case_select_table_1.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Response> + <Query string="Select Bag, Number from Box with Bag" results="2"> + <Role /> + <Entity>Box</Entity> + <Selection> + <Selector name="Bag" /> + <Selector name="Number" /> + </Selection> + </Query> + <Record id="242"> + <Property id="117" name="Number" datatype="TEXT" importance="FIX"> + 0284 + </Property> + <Property id="104" name="Bag" datatype="Bag" importance="FIX"> + 6366 + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + <Value>6406</Value> + <Value>6407</Value> + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + <Value>6408</Value> + <Value>6409</Value> + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + <Value>6410</Value> + <Value>6411</Value> + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + <Value>6412</Value> + <Value>6413</Value> + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + </Property> + </Record> + <Record id="2112"> + <Property id="117" name="Number" datatype="TEXT" importance="FIX"> + 1101 + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + </Property> + </Record> +</Response>