diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 07f72610a0165bb0c221671c922a0ce9232bfbe3..548f86457c3216f5eed7b75b4b81d7f048351248 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,9 +21,10 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. variables: - CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-webui/testenv - # When using dind, it's wise to use the overlayfs driver for - # improved performance. + DEPLOY_REF: dev + CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-webui/testenv + # When using dind, it's wise to use the overlayfs driver for + # improved performance. image: $CI_REGISTRY_IMAGE:latest @@ -66,10 +67,11 @@ trigger_build: - echo $TOKEN - /usr/bin/curl -X POST -F token=$DEPLOY_TRIGGER_TOKEN + -F "variables[F_BRANCH]=$CI_COMMIT_REF_NAME" -F "variables[WEBUI]=$CI_COMMIT_REF_NAME" -F "variables[TriggerdBy]=WEBUI" -F "variables[TriggerdByHash]=$CI_COMMIT_SHORT_SHA" - -F ref=dev https://gitlab.indiscale.com/api/v4/projects/14/trigger/pipeline + -F ref=$DEPLOY_REF https://gitlab.indiscale.com/api/v4/projects/14/trigger/pipeline # Build a docker image in which tests for this repository can run build-testenv: diff --git a/CHANGELOG.md b/CHANGELOG.md index 18052e718ec41d79b7d95c71ce296d83d50856f2..cdc0bbfb39e53824ff52ea968b8926f4eeb413b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +### Security (in case of vulnerabilities) + +## [v0.2.1] - 2020-09-07 + +### Added (for new features, dependecies etc.) + +* `ext_jupyterdrag` (v0.1) module for dragging entities into jupyter notebooks. + +### Changed (for changes in existing functionality) + +### Deprecated (for soon-to-be removed features) + +### Removed (for now removed features) + +### Fixed + +### Security (in case of vulnerabilities) + +## [v0.2] - 2020-09-02 + +### Added (for new features, dependecies etc.) + +* Build variable `EXT_REFERENCES_CUSTOM_REFERENCE_RESOLVER`. The value of this + variable must be module which has at least a `resolve(id)` function, which + returns a `reference_info` object for further processing by the + `resolve_references` module. +* `ext_sss_markdown` module for pretty display of server-side scripting stdout. + See module docstring for more information. +* `ext_trigger_crawler_form` module which generates a form for triggering the + crawler. See module docstring for more info. +* Support for deeply nested selectors in SELECT queries. +* edit_mode supports reference properties when inserting/updating properties + (See [#83](https://gitlab.com/caosdb/caosdb-webui/-/issues/83) and + [#55](https://gitlab.com/caosdb/caosdb-webui/-/issues/55)). +* new `navbar.add_tool` method which adds a button to submenu of the navbar. +* new CSS classes for the styling of the default image and video preview of the + `ext_bottom_line` module: `caosdb-v-bottom-line-image-preview` and + `caosdb-v-bottom-line-video-preview`. +* basic support for entity versioning: + * entity.xsl generates a versioning button which opens a versioning info modal. + * `preview` also works for versioned references + * `edit_mode` prevents the user from editing old versions of an entity. + +### Changed (for changes in existing functionality) +- The `navbar.add_button` signatur changed to `add_button(button, options)`. + See the doc string of that method for more information. +- added a layout argument to the create_plot function of ext_bottom_line + +### Deprecated (for soon-to-be removed features) + +* css class `caosdb-property-text-value` is deprecated because different + functionality interpreted it differently and most of the uses of this class + have already been removed and replaced by specialized classes. + +### Removed (for now removed features) + +### Fixed + * #101 - loading of A LOT of references in `ext_references` slows down the webui or even freezes the brower. +* Fixed a bug where the Tour would lose its state upon page reload. ### Security (in case of vulnerabilities) -## v0.2-rc.1 (2020-04-10) +## [v0.2-rc.1] - 2020-04-10 ### Added (for new features, dependecies etc.) diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 6185725aefd266b45ef5024bd3e47a78801fef1f..23b1e9ff929b030750f6cab0014f22cd0d6b7cf4 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -39,10 +39,12 @@ # overridden in the makefile in any case. ############################################################################## -# Modules enabled by default +# Modules enabled/disabled by default ############################################################################## BUILD_MODULE_EXT_PREVIEW=ENABLED BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED +BUILD_MODULE_EXT_SSS_MARKDOWN=DISABLED +BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=DISABLED ############################################################################## # Navbar properties @@ -76,4 +78,7 @@ BUILD_FOOTER_CUSTOM_ELEMENT_TWO= # BUILD_FOOTER_CUSTOM_ELEMENT_TWO=$(cat footer_element_2.html) BUILD_CUSTOM_IMPRINT='<p> Put an imprint note here </p>' - +############################################################################## +# ext_trigger_crawler_form properties +############################################################################## +BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX="Tools" diff --git a/makefile b/makefile index d9091f32004cfe9c25e28c9e75a3cc18471ac73b..50dd72b500724bbe40801292ab39c3c5cf7ce70f 100644 --- a/makefile +++ b/makefile @@ -92,7 +92,7 @@ run-test-server: test $(MISC_DIR)/unit_test_http_server.py $(PORT) $(TIMEOUT) False $(PUBLIC_DIR) keep-test-server: test - $(MISC_DIR)/unit_test_http_server.py $(PORT) $(TIMEOUT) True $(PUBLIC_DIR) + $(MISC_DIR)/unit_test_http_server.py $(PORT) -1 True $(PUBLIC_DIR) run-qunit: test $(foreach exec, firefox Xvfb xwd,\ diff --git a/misc/select_query_test_data.py b/misc/select_query_test_data.py new file mode 100755 index 0000000000000000000000000000000000000000..3ecf0a47a56641c1c2a05ea3346e9a4ef2ff0e35 --- /dev/null +++ b/misc/select_query_test_data.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 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 + +import caosdb + + +d = caosdb.execute_query("FIND Test*") +if len(d) > 0: + d.delete() + +rt_house = caosdb.RecordType("TestHouse").insert() +rt_house.description = "A House" +caosdb.RecordType("TestWindow").insert() +rt_person = caosdb.RecordType("TestPerson").insert() +caosdb.RecordType("TestParty").insert() +caosdb.Property("TestHeight", datatype=caosdb.DOUBLE, unit="ft").insert() +caosdb.Property("TestDate", datatype=caosdb.DATETIME).insert() + +window = caosdb.Record().add_parent("TestWindow") +window.add_property("TestHeight", 20.5, unit="ft") +window.insert() + +owner = caosdb.Record("The Queen").add_parent("TestPerson").insert() + +house = caosdb.Record("Buckingham Palace") +house.description = "A rather large house" +house.add_parent("TestHouse") +house.add_property(rt_person, name="TestOwner", value=owner) +house.add_property("TestWindow", window).insert() + +g1 = caosdb.Record().add_parent("TestPerson").insert() +g2 = caosdb.Record().add_parent("TestPerson").insert() +g3 = caosdb.Record().add_parent("TestPerson").insert() + +party = caosdb.Record("Diamond Jubilee of Elizabeth II").add_parent("TestParty") +party.add_property(rt_house, name="Location", value=house) +party.add_property("TestDate", "2012-02-06") +party.add_property(rt_person, datatype=caosdb.LIST(rt_person), name="Guests", + value = [g1, g2, g3]) +party.insert() diff --git a/misc/unit_test_http_server.py b/misc/unit_test_http_server.py index c52f84e4ced6da2289dc853c6b65c3c9dc6ed202..68e6a7434a584a26a242e5891329f3fb6c6d159f 100755 --- a/misc/unit_test_http_server.py +++ b/misc/unit_test_http_server.py @@ -115,7 +115,7 @@ class UnitTestHTTPServer(HTTPServer): def __init__(self, server_address, timeout, ignore_done): super(UnitTestHTTPServer, self).__init__(server_address, UnitTestsHandler) - self.timeout = timeout + self.timeout = timeout if timeout > 0 else None self.ignore_done = ignore_done self._keep_running = True self._exit_message = None @@ -139,8 +139,12 @@ class UnitTestHTTPServer(HTTPServer): Start the server and handle request until the `done` resource is being called or a timeout occurs. """ - print(("starting UnitTestHTTPServer on {address} with {t}s " - "timeout.").format(address=self.server_address, t=self.timeout)) + timeout_str = "." + if self.timeout is not None: + timeout_str = " with {}s timeout.".format(self.timeout) + print(("starting UnitTestHTTPServer on {address}" + "{timeout_str}").format(address=self.server_address, + timeout_str=timeout_str)) self._keep_running = True while self._keep_running: self.handle_request() diff --git a/misc/versioning_test_data.py b/misc/versioning_test_data.py new file mode 100755 index 0000000000000000000000000000000000000000..eaa83e46f61ea2f20263b487e4bb42c37678c94f --- /dev/null +++ b/misc/versioning_test_data.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 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 + +""" Insert test data for manually testing the versioning feature in the +webinterface. +""" +# pylint: disable=no-member + +import caosdb + +# clean +old = caosdb.execute_query("FIND Test*") +if len(old) > 0: + old.delete() + +# data model + +rt = caosdb.RecordType("TestRT") +rt.insert() + + +# test data +## record with several versions +rec1 = caosdb.Record("TestRecord1-firstVersion").add_parent("TestRT") +rec1.description = "This is the first version." +rec1.insert() + +rec1.name = "TestRecord1-secondVersion" +rec1.description = "This is the second version." +rec1.update() +if rec1.version: + ref = str(rec1.id) + "@" + rec1.version.id +else: + ref = rec1.id + +rec1.name = "TestRecord1-thirdVersion" +rec1.description = "This is the third version." +rec1.update() + +rec2 = caosdb.Record("TestRecord2").add_parent("TestRT") +rec2.description = ("This record references the TestRecord1 in the second " + "version where the name and the description of the record " + "should indicate the referenced version.") +rec2.add_property("TestRT", ref) +rec2.insert() + +rec3 = caosdb.Record("TestRecord3").add_parent("TestRT") +rec3.description = ("This record references the TestRecord1 without " + "specifying a version. Therefore the latest (at least the " + "third version) is being openend when you click on the " + "reference link.") +rec3.add_property("TestRT", rec1.id) +rec3.insert() + +rec4 = caosdb.Record("TestRecord4").add_parent("TestRT") +rec4.description = ("This record has a list of references to several versions " + "of TestRecord1. The first references the record without " + "specifying the version, the other each reference a " + "different version of that record.") +if rec1.version: + rec4.add_property("TestRT", datatype=caosdb.LIST("TestRT"), + value=[rec1.id, + str(rec1.id) + "@HEAD", + str(rec1.id) + "@HEAD~1", + str(rec1.id) + "@HEAD~2"]) +else: + rec4.add_property("TestRT", datatype=caosdb.LIST("TestRT"), + value=[rec1.id, + str(rec1.id), + str(rec1.id), + str(rec1.id)]) +rec4.insert() diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index d598e8175c249fc1457e04498ffa1af6c3f0654d..05e36429b7dfe6144915b522de2fa6c36375c484 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -27,6 +27,47 @@ body { flex-direction: column; } +.caosdb-v-navbar-toolbox li a:hover, +.caosdb-v-navbar-toolbox li input:hover, +.caosdb-v-navbar-toolbox li button:hover { + background-color: #f5f5f5; +} + +.caosdb-v-navbar-toolbox li a, +.caosdb-v-navbar-toolbox li input, +.caosdb-v-navbar-toolbox li button { + display: block; + padding: 3px 20px; + border: none; + width: 100%; + text-align: left; + margin-top: 0px; + margin-bottom: 0px; + text-decoration: none; + background-color: transparent; + white-space: nowrap; +} + +.caosdb-v-bottom-line-image-preview > img { + max-width: 400px; + max-height: 280px; +} + +.caosdb-v-bottom-line-image-preview { + text-align: center; +} + +button.caosdb-v-entity-version-button { + height: 15px; +} + +.caosdb-v-entity-header-buttons-list > * { + margin: 0; + margin-left: 8px; + padding: 0; + vertical-align: middle; +} + .caosdb-v-main-col { flex-grow: 1; max-width: 90vw; @@ -49,6 +90,9 @@ body { display: initial; } +/* DEPRECATED css class .caosdb-property-text-value - Use +* .caosdb-f-property-single-raw-value or introduce new +* .caosdb-v-property-text-value */ .caosdb-property-text-value { white-space: pre-line; } diff --git a/src/core/js/autocomplete.js b/src/core/js/autocomplete.js index 542849c5e77f764717bad759ca6076550cd85b2f..345d06b37bc6456645ef02947c70b3f2f5d6c44f 100644 --- a/src/core/js/autocomplete.js +++ b/src/core/js/autocomplete.js @@ -101,6 +101,9 @@ var autocomplete = new function() { }; }; -//$(document).ready(function () { -// caosdb_modules.register(autocomplete); -//}); +// this will be replaced by require.js in the future. +$(document).ready(function() { + if ("${BUILD_MODULE_EXT_AUTOCOMPLETE}" === "ENABLED") { + caosdb_modules.register(autocomplete); + } +}); diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js index 648e8249a258a9f723325ec34c230e07a5900514..7fe4e9441822bf6c706a09c95aa65f0a2fef06a8 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -135,17 +135,19 @@ function getEntityRole(element) { } /** - * Return the unit of element. - * If the corresponding data-attribute can not be found undefined is returned. - * @return A string containing the datatype of the element. + * Return the unit of entity or undefined. + * + * @param {HTMLElement} entity + * @return {String} */ -function getEntityUnit(element) { - var res = $(element).find("input.caosdb-f-entity-unit"); +function getEntityUnit(entity) { + var res = $(entity).find("input.caosdb-f-entity-unit"); if (res.length == 1) { var x = res.val(); return (x == '' ? undefined : x); } - return undefined; + + return getEntityHeadingAttribute(entity, "unit"); } /** @@ -245,6 +247,35 @@ function getEntityID(element) { throw new Error("id is ambigous for this element!"); } + +/** + * Get the version string of an entity. + * + * @param {HTMLElement} an Entity in HTML representation + * (div.caosdb-entity-panel) + * @return {string} the version + */ +var getEntityVersion = function (entity) { + return entity.getAttribute("data-version-id"); +} + + +/** + * Get the id and, if present, the version of an entity. + * + * @param {HTMLElement} an Entity in HTML representation + * (div.caosdb-entity-panel) + * @return {string} <id>[@<version>] + */ +var getEntityIdVersion = function (entity) { + const id = getEntityID(entity); + const version = getEntityVersion(entity); + if (version) { + return `${id}@${version}`; + } + return id; +} + /** * Take a date and a time and format it into a CaosDB compatible representation. * @param date A date @@ -631,6 +662,9 @@ function setPropertySafe(valueelement, property, propold) { preview.init(); } } else { + /* DEPRECATED css class .caosdb-property-text-value - Use + * .caosdb-f-property-single-raw-value or introduce new + * .caosdb-v-property-text-value */ valueelement.innerHTML = "<span class='caosdb-property-text-value'>" + property.value + "</span>"; } } @@ -798,7 +832,7 @@ function _createDocument(root) { * @param id The id of the entity. Can be undefined. * @param properties A list of properties. * @param parents A list of parents. - * @return {Document|DocumentFragement} - An xml document holding the newly + * @return {Document|DocumentFragment} - An xml document holding the newly * created entity. * */ diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index b68d8ca4d9d5d309f3374c3717fe001806241173..0c52f403d5d9b2f0b2feb60a9dc21403c12dc0b2 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -188,6 +188,10 @@ var edit_mode = new function() { this.set_entity_dropable = function(entity, dragover, dragleave, parent_drop, property_drop) { + if (getEntityRole(entity) === "Property") { + // currently no parents and subproperties for properties. + return; + } var rts = entity.getElementsByClassName("caosdb-entity-panel-body"); for (var rel of rts) { rel.addEventListener("dragleave", dragleave); @@ -313,6 +317,71 @@ var edit_mode = new function() { } + /** + * Pure converter from a datatype in string representation to a json + * objects. + * + * E.g. passing "LIST<Person>" results in + * { atomic_datatype: "REFERENCE", + * reference_scope: "Person", + * is_list: true + * } + * + * @param {str} datatype + * @returns {object} + */ + this.get_datatype_from_string = function (datatype) { + + var atomic = datatype; + const is_list = edit_mode.isListDatatype(datatype); + if (is_list) { + // unwrap the datatype from LIST<...> + atomic = edit_mode.unListDatatype(datatype); + } + + var ref = undefined; + if (edit_mode._known_atomic_datatypes.indexOf(atomic) == -1) { + // `atomic` not in `known_atomic_datatypes` (e.g. "Person") -> this + // is a reference and the `atomic` is actually the reference's + // scope entity. + ref = atomic; + atomic = "REFERENCE"; + } + + return { "atomic_datatype": atomic, "reference_scope": ref, "is_list": is_list}; + } + + /** + * Pure converter from a datatype json objects to a string representation + * of the datatype. + * + * E.g. passing + * { atomic_datatype: "REFERENCE", + * reference_scope: "Person", + * is_list: true + * } + * results in "LIST<Person>". + * + * @param {object} datatype + * @returns {str} + */ + this.get_datatype_str = function (datatype) { + var result = datatype["atomic_datatype"]; + + const ref = datatype["reference_scope"]; + if (ref !== null && typeof ref !== "undefined") { + result = ref; + } + + const is_list = datatype["is_list"]; + if (is_list === "on" || is_list === true) { + result = `LIST<${result}>`; + } + + return result; + } + + /** * Insert entities. * @@ -324,21 +393,32 @@ var edit_mode = new function() { ent_elements = caosdb_utils.assert_array(ent_elements, "param `ent_elements`", true); var xmls = []; for ( const ent_element of ent_elements ) { - xmls.push(createEntityXML( - getEntityRole(ent_element), - getEntityName(ent_element), - undefined, - edit_mode.getProperties(ent_element), - getParents(ent_element), - true, - getEntityDatatype(ent_element), - getEntityDescription(ent_element), - getEntityUnit(ent_element), - )); + xmls.push(edit_mode.form_to_xml(ent_element)); } return await insert(xmls); } + /** + * Generate an XML from the form of the edited/newly created entity. + * + * @param {HTMLElement} entity_form - FORM of new/changed entity. + * @returns {Document|DocumentFragment} - An xml document containing the + * entity in XML representation. + */ + this.form_to_xml = function(entity_form) { + const obj = form_elements.form_to_object($(entity_form).find("form")[0]); + return createEntityXML( + getEntityRole(entity_form), + getEntityName(entity_form), + getEntityID(entity_form), + edit_mode.getProperties(entity_form), + getParents(entity_form), + true, + edit_mode.get_datatype_str(obj), + getEntityDescription(entity_form), + obj.unit, + ); + } /** * TODO merge with getPropertyFromElement in caosdb.js @@ -415,17 +495,7 @@ var edit_mode = new function() { } this.update_entity = async function(ent_element) { - var xml = createEntityXML( - getEntityRole(ent_element), - getEntityName(ent_element), - getEntityID(ent_element), - edit_mode.getProperties(ent_element), - getParents(ent_element), - true, - getEntityDatatype(ent_element), - getEntityDescription(ent_element), - getEntityUnit(ent_element), - ); + var xml = edit_mode.form_to_xml(ent_element); return await edit_mode.update(xml); } @@ -530,8 +600,8 @@ var edit_mode = new function() { roleElem.detach(); var parentsElem = $(header).find('.caosdb-f-parent-list'); parentsElem.detach(); - var temp = $('<div class="form-group"><label class="col-sm-2 control-label">parents</label><div class="col-sm-10"></div></div>'); - temp.find("div.col-sm-10").append(parentsElem); + const parentsSection = $('<div class="form-group"><label class="col-sm-2 control-label">parents</label><div class="col-sm-10"></div></div>'); + parentsSection.find("div.col-sm-10").append(parentsElem); header.attr("title", "Drop parents from the right panel here."); header.data("toggle", "tooltip"); @@ -539,25 +609,29 @@ var edit_mode = new function() { // create inputs var inputs = [ roleElem, - temp, + parentsSection, this.make_input("name", getEntityName(entity)), this.make_input("description", getEntityDescription(entity)), ]; if (getEntityRole(roleElem[0]) == "Property") { - // TODO refactor getEntityUnit and use that - var unit = getEntityHeadingAttribute(entity, "unit"); - for (const input of this.make_datatype_input(getEntityDatatype(entity), unit)) { - inputs.push(input); - } - temp.hide(); + const current_datatype = getEntityDatatype(entity); + inputs.push(edit_mode.make_datatype_input(current_datatype)); + + const current_unit = getEntityUnit(entity); + inputs.push(edit_mode.make_unit_input(current_unit)); + + // TODO currently no parents for properties. Why not? + parentsSection.hide(); + header.attr("title", ""); } else if (getEntityRole(roleElem[0]) == "File") { inputs.push(this.make_input("path", getEntityPath(entity))); } // remove other stuff header.children().remove(); - header.append($('<form class="form-horizontal"></form>').append(inputs)); - edit_mode.make_dataype_input_logic(header); + const form = $('<form class="form-horizontal"></form>').append(inputs); + header.append(form); + edit_mode.make_datatype_input_logic(form[0]); edit_mode.add_parent_delete_buttons(header[0]); } @@ -569,46 +643,155 @@ var edit_mode = new function() { return datatype.substring(5, datatype.length - 1); } - this.make_dataype_input_logic = function(header) { - var unitLabel = $(header).find(".caosdb-f-entity-unit-label"); - var unitInput = $(header).find(".caosdb-f-entity-unit"); - var isListLabel = $(header).find(".caosdb-f-entity-is-list-label"); - var isListInput = $(header).find(".caosdb-f-entity-is-list"); - var referenceLabel = $(header).find(".caosdb-f-entity-reference-label"); - var referenceInput = $(header).find(".caosdb-f-entity-reference"); + /** + * Append listeners to all input elements depending on the datatype. + * + * The relevant input elements are those for the unit and the + * reference_scope. + * + * Only when the atomic_datatype is either "DOUBLE" or "INTEGER" the unit + * field should be visible and enabled. Only when the atomic_datatype is + * "REFERENCE", the reference_scope field shoudl be visible and enabled. + * + * The listener `on_datatype_change` is added to the datatype field and + * triggered right away for the first time. + * + * @param {HTMLElement} form - The form containing the input fields. + */ + this.make_datatype_input_logic = function(form) { + const datatype = form_elements.get_fields(form, "atomic_datatype"); - // TODO show on reference - referenceInput.hide(); - referenceLabel.hide(); + $(datatype).find("select").change(function () { + const new_type = $(this).val(); + logger.debug(`datatype changed to ${new_type}.`); + edit_mode.on_datatype_change(form, new_type); + }); - // TODO show unit for double and integer + // trigger for the first time + edit_mode.on_datatype_change(form, $(datatype).find("select").val()); } - this.make_datatype_input = function(datatype, unit) { - var is_list = edit_mode.isListDatatype(datatype); - if (is_list) { - datatype = edit_mode.unListDatatype(datatype); - } + /** + * The listener which is added by `make_datatype_input_logic`. + * + * @param {HTMLElement} form - The form containing the datatype and unit + * input elements. + * @param {string} new_type - the new datatype which is used by this + * listener to determine which fields need to be enabled or disabled. + */ + this.on_datatype_change = function (form, new_type) { + logger.trace('enter on_datatype_change', form, new_type); - // datatypes {name: has_unit?} - var datatypes = { - "TEXT": false, - "DOUBLE": true, - "INTEGER": true, - "DATETIME": false, - "BOOLEAN": false, - "FILE": false, - /*TODO "REFERENCE":false*/ + if (new_type === "REFERENCE") { + form_elements.enable_name(form, "reference_scope"); + } else { + form_elements.disable_name(form, "reference_scope"); } - var select = $('<select></select>'); - for (const dt of Object.keys(datatypes)) { - select.append('<option data-has-refid="' + (dt == "REFERENCE") + '" data-has-unit="' + datatypes[dt] + '" value="' + dt + '" ' + (dt == datatype ? 'selected="true"' : '') + '>' + dt + '</option>'); + + if (new_type === "DOUBLE" || new_type === "INTEGER") { + form_elements.enable_name(form, "unit"); + } else { + form_elements.disable_name(form, "unit"); } + } + - return [ - $('<div class="form-group"><label class="col-sm-2 control-label caosdb-f-entity-datatype-label">datatype</label><div class="col-sm-3"><select class="form-control caosdb-f-entity-datatype">' + select.html() + '</select></div><label class="col-sm-2 control-label caosdb-f-entity-reference-label">reference</label><div class="col-sm-3"><input readonly="true" class="form-control caosdb-f-entity-reference" value="" placeholder="Drop a RT"></input></div><label class="col-sm-1 control-label caosdb-f-entity-is-list-label">list</label><div class="col-sm-1"><input class="caosdb-f-entity-is-list" type="checkbox" ' + (is_list ? 'checked="true" ' : "") + '/></div>')[0], - $('<div class="form-group"><label class="col-sm-2 control-label caosdb-f-entity-unit-label">unit</label><div class="col-sm-2"><input type="text" class="form-control caosdb-f-entity-unit" value="' + (typeof unit == 'undefined' ? "" : unit) + '"></input></div></div>')[0], + /** + * Generate a text input element for the unit of an abstract property. + * + * @param {string} unit - the initial value of the input element. + * @returns {HTMLElement} - a labeled form field. + */ + this.make_unit_input = function(unit) { + const unit_input = $(form_elements + .make_text_input({ + name: "unit", + label: "unit", + value: unit, + })); + unit_input.toggleClass("form-group", true); + unit_input.find(".col-sm-3").toggleClass("col-sm-2", true).toggleClass("col-sm-3", false); + unit_input.find(".col-sm-9").toggleClass("col-sm-2", true).toggleClass("col-sm-9", false); + return unit_input[0]; + } + + this._known_atomic_datatypes = [ + "TEXT", + "DOUBLE", + "INTEGER", + "DATETIME", + "BOOLEAN", + "FILE", + "REFERENCE", ]; + + /** + * Make three input elements which contain all necessary parts of a datatype. + * + * The three input elements are wrapped in a single DIV.form-group. + * + * @param {string} [datatype] - defaults to TEXT if undefined. + * @returns {HTMLElement} + */ + this.make_datatype_input = function(datatype) { + var _datatype = datatype || "TEXT"; + + // split/convert datatype string into more practical variables. + const datatype_obj = edit_mode.get_datatype_from_string(_datatype); + const atomic_datatype = datatype_obj["atomic_datatype"]; + const reference_scope = datatype_obj["reference_scope"]; + const is_list = datatype_obj["is_list"]; + + + // generate select input for the atomic_datatype + const select = $('<select name="atomic_datatype" class="form-control"></select>'); + for (const dt of edit_mode._known_atomic_datatypes) { + select.append(`<option value="${dt}">${dt}</option>`); + } + select.val(atomic_datatype) + + const datatype_config = { + name: "atomic_datatype", + label: "datatype", + }; + const datatype_selector = $(form_elements + ._make_field_wrapper(datatype_config.name)) + .append(form_elements._make_input_label_str(datatype_config)) + .append($('<div class="col-sm-3"/>').append(select)); + + // generate select input for the RecordTypes which are the reference's + // scope. + const reference_config = { + name: "reference_scope", + label: "reference to", + value: reference_scope, + query: "SELECT name FROM RECORDTYPE", + make_desc: getEntityName, + make_value: getEntityName, + }; + const ref_selector = form_elements + .make_reference_drop_down(reference_config); + + + // generate the checkbox ([ ] list) + const list_checkbox_config = { + name: "is_list", + label: "list", + checked: is_list, + } + const list_checkbox = form_elements + .make_checkbox_input(list_checkbox_config); + + // styling + $(list_checkbox).children().toggleClass("col-sm-3",false).toggleClass("col-sm-9", false).toggleClass("col-sm-1", true); + + const form_group = $('<div class="form-group">').append([datatype_selector, ref_selector, list_checkbox]); + form_group.find(".form-group").toggleClass("form-group", false); + form_group.find(".col-sm-3").toggleClass("col-sm-2", true).toggleClass("col-sm-3", false); + form_group.find(".col-sm-9").toggleClass("col-sm-3", true).toggleClass("col-sm-9", false); + + + return form_group[0]; } this.make_input = function(label, value) { @@ -1168,6 +1351,10 @@ var edit_mode = new function() { const state = app.state; $('.caosdb-entity-panel').each(function(index) { let entity = this; + if ($(entity).is("[data-version-successor]")) { + // not the latest version -> ignore + return; + } // remove listeners from all entities edit_mode.unset_entity_dropable(entity, edit_mode.dragover, edit_mode.dragleave, edit_mode.parent_drop_listener, edit_mode.property_drop_listener); @@ -1202,6 +1389,10 @@ var edit_mode = new function() { edit_mode.enable_new_buttons(); $('.caosdb-entity-panel').each(function(index) { let entity = this; + if ($(entity).is("[data-version-successor]")) { + // not the latest version -> ignore + return; + } if (typeof getEntityID(entity) == "undefined" || getEntityID(entity) == '') { // no id -> insert edit_mode.init_actions_panels(entity); @@ -1400,8 +1591,8 @@ var edit_mode = new function() { /** * Fill the reference drop down menu of an entity property with options. * - * The options are candidates for reference targets (i.e. values of - * reference properties). + * The options are the entities which are within the scope of the reference + * property. * * @param {HTMLElement} drop_down - A SELECT element which will be filled. * @param {HTMLElement[]} options - array of option elements, ready for diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js index 900263366e456c8f7a2945af6a24c7a10b345c0e..1bcf73dea17889bda9858ed78a2f6848e1dc21ee 100644 --- a/src/core/js/ext_bottom_line.js +++ b/src/core/js/ext_bottom_line.js @@ -43,7 +43,7 @@ * @requires getEntityPath (function from caosdb.js) * @requires connection (module from webcaosdb.js) */ -var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEntityPath, connection) { +var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection) { /** * @property {string|function} create - a function with one parameter @@ -83,11 +83,11 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @return {boolean} true iff the entity has a path with one of the * extensionss. */ - const _path_has_file_extension = function (entity, extensions) { + const _path_has_file_extension = function(entity, extensions) { const path = getEntityPath(entity); if (path) { for (var ext of extensions) { - if(path.toLowerCase().endsWith(ext)) { + if (path.toLowerCase().endsWith(ext)) { return true; } } @@ -101,9 +101,9 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @param {HTMLElement} entity * @return {HTMLElement} a VIDEO element. */ - const _create_video_preview = function (entity) { + const _create_video_preview = function(entity) { var path = connection.getFileSystemPath() + getEntityPath(entity); - return $(`<video controls="controls"><source src="${path}"/></video>`)[0]; + return $(`<div class="caosdb-v-bottom-line-video-preview"><video controls="controls"><source src="${path}"/></video></div>`)[0]; } /** @@ -112,9 +112,9 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @param {HTMLElement} entity * @return {HTMLElement} an IMG element. */ - const _create_picture_preview = function (entity) { + const _create_picture_preview = function(entity) { var path = connection.getFileSystemPath() + getEntityPath(entity); - return $(`<img class="entity-image-preview" style="max-width: 200px; max-height=140px;" src="${path}"/>`)[0]; + return $(`<div class="caosdb-v-bottom-line-image-preview"><img src="${path}"/></div>`)[0]; } var fallback_preview = undefined; @@ -124,20 +124,17 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * * @member {Creator[]} */ - const _default_creators = [ - { // pictures + const _default_creators = [{ // pictures id: "_default_creators.pictures", is_applicable: (entity) => _path_has_file_extension( entity, ["jpg", "png", "gif", "svg"]), create: _create_picture_preview - }, - { // videos + }, { // videos id: "_default_creators.videos", is_applicable: (entity) => _path_has_file_extension( entity, ["mp4", "mov", "webm"]), create: _create_video_preview, - }, - { // fallback + }, { // fallback id: "_default_creators.fallback", is_applicable: (entity) => true, create: (entity) => fallback_preview, @@ -167,7 +164,7 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @returns {HTMLElement} the preview container or `undefined` if the entity * doesn't have any. */ - const get_preview_container = function (entity) { + const get_preview_container = function(entity) { return $(entity).children(`.${_css_class_preview_container}`)[0]; } @@ -188,16 +185,20 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @param {HTMLElement} entity - An entity in HTML Representation which * must have a (deep) child with class `caosdb-f-ext_bottom_line-container`. */ - const set_preview_container = function (entity, element) { + const set_preview_container = function(entity, element) { const preview_container = $(get_preview_container(entity)); if (preview_container[0]) { preview_container.empty(); var buttons = preview_container.siblings(`.${_css_class_preview_container_button}`); if (element) { - buttons.css({"visibility": "initial"}); + buttons.css({ + "visibility": "initial" + }); preview_container.append(element); } else { - buttons.css({"visibility": "hidden"}); + buttons.css({ + "visibility": "hidden" + }); } } else { logger.error(new Error("Could not find the preview container.")); @@ -219,7 +220,7 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @param {string|HTMLElement|Promise} preview - A preview for an entity or * a Promise for a preview (which resolves as a string or an HTMLElement as well). */ - var set_preview = async function (entity, preview) { + var set_preview = async function(entity, preview) { try { const wait = "Please wait..."; set_preview_container(entity, wait); @@ -252,7 +253,7 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @returns {String|HTMLElement|Promise} A preview which can be added to * the entity DOM representation or a Promise for such a preview. */ - var root_preview_creator = async function (entity) { + var root_preview_creator = async function(entity) { for (let c of _creators) { try { if (await c.is_applicable(entity)) { @@ -283,15 +284,21 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @param {HTMLElement} entity - An entity in HTML representation. * @return {HTMLElement} the newly created container. */ - var add_preview_container = function (entity) { + var add_preview_container = function(entity) { const button_show = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-down"/> Show Preview</button>') - .css({width: "100%"}) + .css({ + width: "100%" + }) .addClass(_css_class_preview_container_button); const button_hide = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-up"/> Hide Preview</button>') - .css({width: "100%"}) + .css({ + width: "100%" + }) .addClass(_css_class_preview_container_button) .hide(); - const style = { padding: "0px 10px" }; + const style = { + padding: "0px 10px" + }; const container = $(`<div class="collapse"/>`) .addClass(_css_class_preview_container) .addClass(_css_class_preview_container_resolvable) @@ -308,7 +315,9 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti } button_show.click(show); button_hide.click(hide); - container.on("shown.bs.collapse", () => { container[0].dispatchEvent(previewShownEvent); }); + container.on("shown.bs.collapse", () => { + container[0].dispatchEvent(previewShownEvent); + }); $(entity).append(container); $(entity).append(button_show); $(entity).append(button_hide); @@ -326,7 +335,7 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @param {HTMLElement} entity - the entity for which the preview is to * created. */ - var root_preview_handler = async function (entity) { + var root_preview_handler = async function(entity) { var container = $(get_preview_container(entity) || add_preview_container(entity)); if (container.hasClass(_css_class_preview_container_resolvable)) { container.removeClass(_css_class_preview_container_resolvable); @@ -339,7 +348,7 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * Trigger the root_preview_handler for all entities within the view port * when the view port. */ - var root_preview_handler_trigger = function () { + var root_preview_handler_trigger = function() { var entities = $(".caosdb-entity-panel,.caosdb-entity-preview"); for (let entity of entities) { @@ -359,7 +368,7 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * event. After this timeout the trigger is called. * @param {function} trigger - the callback which is called. */ - var init_watcher = function (delay, trigger) { + var init_watcher = function(delay, trigger) { var scroll_timeout = undefined; $(window).scroll(() => { if (!scroll_timeout) { @@ -387,9 +396,9 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * * @param {BottomLineConfig} config */ - var configure = async function (config) { + var configure = async function(config) { logger.debug("configure", config); - if(config.version != "0.1") { + if (config.version != "0.1") { throw new Error("Wrong version in config."); } @@ -421,7 +430,7 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti * @property {BottomLineConfig} [config] - an optional config. Per default, the * configuration is fetched from the server. */ - const init = async function (config) { + const init = async function(config) { logger.info("ext_bottom_line initialized"); try { @@ -458,14 +467,14 @@ var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEnti /** * Helper for plotly */ -var plotly_preview = function (logger, ext_bottom_line, plotly) { +var plotly_preview = function(logger, ext_bottom_line, plotly) { /** * Create a plot with plotly. * * The layout and any other plotly options are set by this function. The - * only parameter `data` is the `data` parameter of the `Plotly.newPlot` - * factory. + * only parameters are `data` and `layout` which are the respective + * parameters of the `Plotly.newPlot` factory. * * Hence the full documentation of the `data` parameter is available at * https://plotly.com/javascript/plotlyjs-function-reference/#plotlynewplot @@ -473,22 +482,33 @@ var plotly_preview = function (logger, ext_bottom_line, plotly) { * * @param {object[]} data - array of objects containing the data which is * to be plotted. + * @param {object[]} layout - dictionary of settings defining the layout of + * the plot. * @returns {HTMLElement} the element which contains the plot. */ - const create_plot = function (data) { + const create_plot = function(data, + layout = { + margin: { + t: 0 + }, + height: 400, + widht: 400 + }) { var div = $('<div/>')[0]; - plotly.newPlot(div, data, { margin: { t: 0}, height: 400, widht: 400 }, {responsive: true}); + plotly.newPlot(div, data, layout, { + responsive: true + }); return div; } - const resize_plots_event_handler = function (e) { + const resize_plots_event_handler = function(e) { var plots = $(e.target).find(".js-plotly-plot"); for (let plot of plots) { plotly.Plots.resize(plot); } } - const init = function () { + const init = function() { window.addEventListener(ext_bottom_line.previewReadyEvent.type, resize_plots_event_handler, true); window.addEventListener(ext_bottom_line.previewShownEvent.type, @@ -504,7 +524,7 @@ var plotly_preview = function (logger, ext_bottom_line, plotly) { // this will be replaced by require.js in the future. -$(document).ready(function () { +$(document).ready(function() { if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") { caosdb_modules.register(plotly_preview); caosdb_modules.register(ext_bottom_line); diff --git a/src/core/js/ext_cosmetics.js b/src/core/js/ext_cosmetics.js index fe1819ed1be69fa390fbf87ae34362efacf24c83..3913a37b6645e88d3a357b9c4c124cf296ccfcad 100644 --- a/src/core/js/ext_cosmetics.js +++ b/src/core/js/ext_cosmetics.js @@ -4,6 +4,9 @@ var cosmetics = new function() { } this.linkify = function() { + /* DEPRECATED css class .caosdb-property-text-value - Use + * .caosdb-f-property-single-raw-value or introduce new + * .caosdb-v-property-text-value */ $('.caosdb-property-text-value').each(function(index) { if (/^https?:\/\//.test(this.innerText)) { var uri = this.innerText; @@ -20,4 +23,4 @@ var cosmetics = new function() { $(document).ready(function() { cosmetics.init(); -}); \ No newline at end of file +}); diff --git a/src/core/js/ext_jupyterdrag.js b/src/core/js/ext_jupyterdrag.js new file mode 100644 index 0000000000000000000000000000000000000000..87d2f3a964ced1411df66da04ff4ddd4d7db2f15 --- /dev/null +++ b/src/core/js/ext_jupyterdrag.js @@ -0,0 +1,76 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 Alexander Schlemmer <alexander.schlemmer@ds.mpg.de> + * + * 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 + */ + +'use strict'; + +/** + * The ext_jupyterdrag module adds dragstart listeners to the WebUI which allow + * dragging single entities into Jupyter notebooks and other text editors (for + * python code) which implement drop gestures for text data. + * + * @module ext_jupyterdrag + * @version 0.1 + * + * @requires jQuery + * @requires log + * @requires getEntityRole + * @requires getEntityID + */ +var ext_jupyterdrag = function($, logger, getEntityRole, getEntityID) { + + + /** + * Initialize the ext_jupyterdrag module. + * + */ + var init = async function() { + $(".caosdb-entity-panel").find( + ".caosdb-entity-panel-heading").attr("draggable", + "true"); + $(".caosdb-entity-panel").find( + ".caosdb-entity-panel-heading").on("dragstart", + function(ev) { + logger.trace("dragstart", ev); + var eel = this.parentElement; + var role = getEntityRole(eel); + var entid = getEntityID(eel); + ev.originalEvent.dataTransfer.setData( + "text/plain", + "db." + role + "(id=" + entid + ")"); + }); + } + + + + return { + // public members, part of the API + init: init, + _logger: logger, + } +}($, log.getLogger("ext_jupyterdrag"), getEntityRole, getEntityID); + + +$(document).ready(function() { + caosdb_modules.register(ext_jupyterdrag); +}); diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index 8676bed637fdf8c8134cc2a3a2aba1e914e2807b..8a3bc50852381e5e9e9c6221b97a54f2f6841cfb 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -54,144 +54,6 @@ var isOutOfViewport = function (elem) { }; -/** - * @module awi_references - * @version 0.1 - * - * Special functionality for AWI. Should be removed from the main repository in - * the future. - * - * @author Timm Fitschen - */ -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); - } -} - /** * @module reference_list_summary * @version 0.1 @@ -459,6 +321,15 @@ var resolve_references = new function () { * the entity which is to be resolved. @return {reference_info} */ this.resolve_reference = async function (id) { + const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"]; + if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") { + // try custom_reference_resolver and fall-back to standard implementation + var ret = await custom_reference_resolver.resolve(id); + if (ret) { + return ret; + } + } + const entity = (await resolve_references.retrieve(id))[0]; // TODO handle multiple parents @@ -475,25 +346,6 @@ var resolve_references = new function () { 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 === "TestReferenced" && typeof resolve_references.test_resolver === "function") { // this is a test case, initialized by the test suite. ret = resolve_references.test_resolver(entity); @@ -651,7 +503,7 @@ var resolve_references = new function () { summary_field.dispatchEvent( resolve_references .summary_ready_event - ); logger.error("here");}) + );}) .catch((err) => { logger.error(err); }) diff --git a/src/core/js/ext_sss_markdown.js b/src/core/js/ext_sss_markdown.js new file mode 100644 index 0000000000000000000000000000000000000000..21c7d3261faeb7052dbe8aa0b9fba29d88ed56d9 --- /dev/null +++ b/src/core/js/ext_sss_markdown.js @@ -0,0 +1,55 @@ +/** + * @module ext_sss_markdown + * @version 0.1 + * + * Transforms the STDOUT of server-side scripting responses from plain text to + * HTML. The STDOUT is interpreted as Markdown. + * + * Module has to be enabled via setting the build property + * `BUILD_MODULE_EXT_SSS_MARKDOWN=ENABLED`. + */ +var ext_sss_markdown = function() { + + var logger = log.getLogger("ext_sss_markdown"); + + var init = function() { + logger.trace("enter init"); + const stdout_container = $("#caosdb-stdout"); + if (stdout_container.length == 0) { + // nothing to do + return; + } + + // get text content of the STDOUT container + const text = stdout_container[0].textContent; + logger.debug("transform", text); + // interpret as markdown. + const html = markdown.textToHtml(text); + + // a little styling + stdout_container.css({padding: 10}); + + // replace plain markdown text with formatted html + stdout_container.empty(); + stdout_container.append(html); + + // hide error output container if the STDERR was empty. + var errortext = $("#caosdb-stderr").text(); + if (errortext.length == 0) { + $("#caosdb-container-stderr").hide(); + } + } + + return { + init: init, + logger: logger, + }; + +}(); + + +$(document).ready(function() { + if ("${BUILD_MODULE_EXT_SSS_MARKDOWN}" == "ENABLED") { + caosdb_modules.register(ext_sss_markdown); + } +}); diff --git a/src/core/js/ext_trigger_crawler_form.js b/src/core/js/ext_trigger_crawler_form.js new file mode 100644 index 0000000000000000000000000000000000000000..94ea6feb672cc3a2be8c87cb6bd6f5c8a5087d3f --- /dev/null +++ b/src/core/js/ext_trigger_crawler_form.js @@ -0,0 +1,132 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * 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 + * 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 + */ + +'use strict'; + +/** + * @module ext_trigger_crawler_form + * @version 0.1 + * + * Adds a button to a configurable navbar toolbox which opens a form for + * triggering the crawler as a server-side scripting job. + * + * The form has two fields. The text field `Path` sets the directory tree which + * is searched by the crawler. The checkbox `Suppress Warnings` can be checked + * to suppress the crawlers warnings. + * + * The module has to be enabled via the + * `BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=ENABLED` build variable. + * + * The toolbox where the button is added can be configured via the build + * variable `BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX`. The default is + * `Tools`. + */ +var ext_trigger_crawler_form = function () { + + var init = function (toolbox) { + const _toolbox = toolbox || "${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX}"; + + const script = "crawl.py" + const button_name = "Trigger Crawler"; + const title = "The LinkAhead-Crawler will run over the filesystem and make necessary updates."; + + const crawler_form = make_scripting_caller_form( + script, button_name); + const modal = make_form_modal(crawler_form); + + + navbar.add_tool(button_name, _toolbox, { + title: title, + callback: () => { + $(modal).modal("toggle"); + } + }); + } + + /** + * Wrap the form into a Bootstrap modal. + */ + var make_form_modal = function (form) { + const title = "Trigger the Crawler"; + const modal = $(` + <div class="modal fade" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" + class="close" + data-dismiss="modal" + aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + <h4 class="modal-title">${title}</h4> + </div> + <div class="modal-body"> + </div> + </div> + </div>`); + modal.find(".modal-body").append(form); + return modal[0]; + } + + /** + * Create the trigger crawler form. + */ + var make_scripting_caller_form = function (script, button_name) { + const path_field = form_elements.make_text_input({ + name: "-p1", + value: "/", + label: "Path" + }); + const warning_checkbox = form_elements.make_checkbox_input({ + name: "-Osuppress", + label: "Suppress Warnings" + }); + $(warning_checkbox).find("input").attr("value", "TRUE"); + + const scripting_caller = $(` + <form method="POST" action="/scripting"> + <input type="hidden" name="call" value="${script}"/> + <input type="hidden" name="-p0" value=""/> + <div class="form-group"> + <input type="submit" + class="form-control btn btn-primary" value="${button_name}"/> + </div> + </form>`); + + scripting_caller.prepend(warning_checkbox).prepend(path_field); + + return scripting_caller[0]; + } + + return { + init: init, + }; + +}(); + +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM}" === "ENABLED") { + caosdb_modules.register(ext_trigger_crawler_form); + } +}); diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index 5c29058f532dc30daa98b6354b60e3caab88a7a6..0014f4db452a55839edaa860d7fa49f98529434e 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -23,7 +23,10 @@ 'use strict'; /** - * caosdb_map module for reusable form elemenst which already have a basic css styling. + * form_elements module for reusable form elemenst which already have a basic + * css styling. + * + * @version 0.2 * * IMPORTANCE CONCEPTS * @@ -196,21 +199,32 @@ var form_elements = new function () { /** * Return SELECT form element with entity references. * - * The OPTIONS have the entities' ids as values and a description which - * is generated by a `make_desc` call-back function. If `make_desc` is - * undefined, the ids are shown instead. + * The OPTIONS' values are generated by the `make_value` call-back + * function from the entities. If `make_value` is undefined the + * entities' ids are used as values. The description which is generated + * by a `make_desc` call-back function. If `make_desc` is undefined, + * the ids are shown instead. * * @param {HTMLElement[]} entities - an array with entity elements. * @param {function} [make_desc] - a call-back function with one - * parameter. + * parameter which is an entity in HTML representation. + * @param {function} [make_value] - a call-back function with one + * parameter which is an entity in HTML representation. + * @param {boolean} [multiple] - whether the select allows multiple + * options to be selected. * @returns {HTMLElement} SELECT element with entity options. */ - this.make_reference_select = async function (entities, make_desc, multiple = false) { + this.make_reference_select = async function (entities, make_desc, + make_value, multiple) { caosdb_utils.assert_array(entities, "param `entities`", false); if (typeof make_desc !== "undefined") { caosdb_utils.assert_type(make_desc, "function", "param `make_desc`"); } + if (typeof make_value !== "undefined") { + caosdb_utils.assert_type(make_value, "function", + "param `make_value`"); + } const ret = $('<select class="selectpicker form-control" title="Nothing selected"/>'); if (multiple) { ret.attr("multiple", ""); @@ -222,26 +236,57 @@ var form_elements = new function () { let entity_id = getEntityID(entity); let desc = typeof make_desc == "function" ? await make_desc(entity) : entity_id; - let option = this.make_reference_option(entity_id, desc); + let value = typeof make_value == "function" ? await make_value(entity) : entity_id; + let option = this.make_reference_option(value, desc); ret.append(option); } return ret[0]; } + /** + * @typedef {option} ReferenceDropDownConfig + * + * Configuration object for a drop down menu for selecting references. + * `make_reference_drop_down` generates such a drop down menu using a + * SELECT input with the references as its OPTION elements. + * + * The `query` parameter contains a query which is executed. The + * resulting entities are used to generate the OPTIONs. + * + * The OPTIONs' values are generated by the `make_value` call-back + * function from the entities. If `make_value` is undefined the + * entities' ids are used as values. The description which is generated + * by a `make_desc` call-back function. If `make_desc` is undefined, + * the ids are shown instead. + * + * The generated HTMLElements also contain a LABEL tag showing the text + * defined by `label`. If the `label` property is undefined, the `name` + * is shown instead. + * + * @property {string} name - The name of the select input. + * @property {string} query - Query for entities. + * @property {function} [make_value] - Call-back for the generation of + * the OPTIONs' values from the entities. + * @property {function} [make_desc] - Call-back for the generation of + * the OPTIONs' description from the entities. + * @property {boolean} [multiple] - Whether it is possible to select + * multiple options at once. + * @property {string} [value] - Pre-selected value of the SELECT. + * @property {string} [label] - LABEL text. + * @property {string} [type] - This should be "reference_drop_down" or + * undefined. This property is used by `make_form_field` to decide + * which type of field is to be generated. + * + */ /** * Search and retrieve entities and create a SELECT from element. * - * The OPTIONS have the entities' ids as values and a description which - * is generated by a `make_desc` call-back function. If `make_desc` is - * undefined, the ids are shown instead. - * - * @param {object} config + * @param {ReferenceDropDownConfig} config - all necessary parameters + * for the configuration. * @returns {HTMLElement} SELECT element. - * - * TODO make syncronous */ - this.make_reference_drop_down = async function (config) { + this.make_reference_drop_down = function (config) { let ret = $(this._make_field_wrapper(config.name)); let label = this._make_input_label_str(config); let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>'); @@ -250,11 +295,12 @@ var form_elements = new function () { input_col.append(loading); this._query(config.query).then(async function (entities) { let select = $(await form_elements.make_reference_select( - entities, config.make_desc, config.multiple)); + entities, config.make_desc, config.make_value, config.multiple, + config.value)); select.attr("name", config.name); loading.remove(); input_col.append(select); - form_elements.init_select_picker(ret[0]); + form_elements.init_select_picker(ret[0], config.value); ret[0].dispatchEvent(form_elements.field_ready_event); select.change(function () { ret[0].dispatchEvent(form_elements.field_changed_event); @@ -270,7 +316,7 @@ var form_elements = new function () { } - this.init_select_picker = function (field) { + this.init_select_picker = function (field, value) { caosdb_utils.assert_html_element(field, "parameter `field`"); const select = $(field).find("select")[0]; const select_picker_options = {}; @@ -283,6 +329,7 @@ var form_elements = new function () { select_picker_options["liveSearchPlaceholder"] = "search..."; } $(select).selectpicker(select_picker_options); + $(select).selectpicker("val", value); this.init_actions_box(field); } @@ -486,7 +533,7 @@ var form_elements = new function () { } else if (type === "range") { field = await this.make_range_input(config); } else if (type === "reference_drop_down") { - field = await this.make_reference_drop_down(config); + field = this.make_reference_drop_down(config); } else if (type === "subform") { // TODO handle cache and required for subforms return await this.make_subform(config); @@ -664,11 +711,11 @@ var form_elements = new function () { } this.enable_name = function (form, name) { - this.enable_fields(form.find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } this.disable_name = function (form, name) { - this.disable_fields(form.find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } this.make_script_form = async function (config, script) { @@ -963,7 +1010,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-f-entity-property" data-field-name="' + name + '" />') + return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />') .css({"padding": "0"})[0]; } @@ -1133,7 +1180,7 @@ var form_elements = new function () { let ret = $(this._make_field_wrapper(config.name)); let name = config.name; let label = this._make_input_label_str(config); - let type = config.type; + let type = config.type || "text"; let value = config.value; let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + '" name="' + name + diff --git a/src/core/js/preview.js b/src/core/js/preview.js index 9af9bb0366097f6d1017a46ff077874ab34bf773..f74fd13ffd2fe373543c025866e91cfe6b6fd9eb 100644 --- a/src/core/js/preview.js +++ b/src/core/js/preview.js @@ -30,9 +30,25 @@ var preview = new function() { return props; } + // import from global name space. + this.getEntityID = getEntityID; + this.getEntityVersion = getEntityVersion; + this.getEntityIdVersion = getEntityIdVersion; + + /** + * Get the id and, if present, the version of an entity from a link or a + * displayed reference. + * + * @param {HTMLElement} a link to an entity. + * @return {string} <id>[@<version>] + */ + this.getEntityRef = function (link) { + return link.getElementsByClassName("caosdb-id")[0].textContent; + } + /** * Initialize the preview feature for all reference properties which belong to certain entity. - * + * * @param {HTMLElement} entity * @return {HTMLElement[]} The initialized properties. */ @@ -113,7 +129,7 @@ var preview = new function() { app.onEnterWaiting = function(e) { executeFailSave(function() { preview.addWaitingNotification(ref_property_elem, preview.createWaitingNotification()); - let entityIds = preview.getEntityIds(refLinksContainer); + let entityIds = preview.getAllEntityRefs(refLinksContainer); preview.retrievePreviewEntities(entityIds).then(entities => { app.receivePreview(entities); }, err => { @@ -391,9 +407,9 @@ var preview = new function() { let selectorButtons = preview.getSelectorButtons(nav); selectorButtons.each((index, button) => { let slide_id = button.getAttribute("data-slide-to"); - let entity_id = getEntityId(button); - let entity = preview.getEntityById(entities, entity_id); - if (entity == null) throw new Error("Entity with ID " + entity_id + " could not be found!"); + let entity_id_version = preview.getEntityRef(button); + let entity = preview.getEntityByIdVersion(entities, entity_id_version); + if (entity == null) throw new Error("Entity with ID " + entity_id_version + " could not be found!"); inner.children[slide_id].appendChild(preview.preparePreviewEntity(entity)); }); @@ -443,8 +459,8 @@ var preview = new function() { * */ this.createSinglePreview = function(entities, refLinksContainer) { - let entityId = getEntityId(preview.getReferenceLinks(refLinksContainer)[0]); - let entity = preview.preparePreviewEntity(preview.getEntityById(entities, entityId)); + const entityRef = preview.getEntityRef(preview.getReferenceLinks(refLinksContainer)[0]); + const entity = preview.preparePreviewEntity(preview.getEntityByIdVersion(entities, entityRef)); return entity; } @@ -456,15 +472,25 @@ var preview = new function() { * @return {HTMLElement} The prepared entity. */ this.preparePreviewEntity = function(entity) { + // move version modal into body because otherwise it would be displayed + // inside the caroussel. That would make sense but there is simply not + // enough space. + $(entity).find(".caosdb-f-entity-version-info").appendTo(document.body); + + + // make backref button smaller + $(entity).find(".caosdb-backref-link > .hidden-xs").hide(); + var preparedEntity = entity.cloneNode(true); // header is clickable: - let href = connection.getBasePath() + transaction.generateEntitiesUri([getEntityId(entity)]); + let href = connection.getBasePath() + transaction.generateEntitiesUri([preview.getEntityRef(entity)]); let link = $('<a title="Load this entity in a new window." href="' + href + '" class="label caosdb-id caosdb-id-button" target="_blank"></a>'); let entityIdElem = $(preparedEntity).find('.label.caosdb-id'); link.insertAfter(entityIdElem); link.append(entityIdElem.text() + " "); link.append('<span class="glyphicon glyphicon-new-window"/>'); + // TODO this link is not visible due to webcaosdb.css (caosdb-id) entityIdElem.remove(); return preparedEntity; @@ -529,22 +555,34 @@ var preview = new function() { } /** - * Get the entity with a certain ID from an array of entities. Returns null if no such entity - * is in the array. + * Get the entity with a certain ID and Version (if applicable) from an + * array of entities. Returns null if no such entity is in the array. + * * @param {HTMLElement[]} entities - * @param {Number} entity_id - * @return {HTMLElement} The entity with id=entity_id or null. + * @param {String} entity_id_version + * @return {HTMLElement} Matching entity or null. */ - this.getEntityById = function(entities, entity_id) { + this.getEntityByIdVersion = function(entities, entity_id_version) { if (entities == null) { throw new Error("entities must not be null"); } - if (entity_id == null || isNaN(entity_id)) { - throw new Error("entity_id is to be a number"); + if (entity_id_version == null) { + throw new Error("entity_id_version must not be null"); + } + + // if the entity_id_version contains an "@" it is actually a reference + // to a versioned entity. Otherwise, it is just an id an thus only the + // id has to be matched. + const is_versioned = entity_id_version.indexOf("@") !== -1; + var matches; + if (is_versioned) { + matches = (e) => preview.getEntityIdVersion(e) === entity_id_version; + } else { + matches = (e) => preview.getEntityID(e) === entity_id_version; } for (let i = 0; i < entities.length; i++) { - let e = entities[i]; - if (getEntityId(e) === entity_id) { + const e = entities[i]; + if (matches(e)) { return e; } } @@ -694,26 +732,27 @@ var preview = new function() { * @param {HTMLElement} refLinksContainer * @return {String[]} An array of entity ids. */ - this.getEntityIds = function(refLinksContainer) { + this.getAllEntityRefs = function(refLinksContainer) { if (refLinksContainer == null) { throw new Error("parameter refLinksContainer must not be null."); } - let entityIds = []; - preview.getReferenceLinks(refLinksContainer).each((index, link) => { - entityIds.push(getEntityId(link)); - }); - return entityIds; + let entityRefs = []; + for (let link of preview.getReferenceLinks(refLinksContainer)) { + entityRefs.push(preview.getEntityRef(link)); + }; + return entityRefs; } /** * Get an array of all reference links. * * @param {HTMLElement} refLinksContainer - The original reference links. - * @return {jQuery} A collection of links. + * @return {HTMLElement[]} A collection of links. */ this.getReferenceLinks = function(refLinksContainer) { - return $(refLinksContainer).find('a').addBack('a').has('.caosdb-id'); + return $(refLinksContainer) + .find('a').addBack('a').has('.caosdb-id').toArray(); } }; diff --git a/src/core/js/query_shortcuts.js b/src/core/js/query_shortcuts.js index c12e2b47b7c8b970b2a8177ebdb529dd5cb47efa..426a6db4ea3b25d10e0bb6c0289caa664b34e71f 100644 --- a/src/core/js/query_shortcuts.js +++ b/src/core/js/query_shortcuts.js @@ -824,20 +824,38 @@ var query_shortcuts = new function() { fields: this.make_create_fields(entity[0]), }; var form = form_elements.make_form(form_config); + this._toggle_entity_property_class(form); + this.logger.trace("leave make_create_form", form); return form; } + /** + * Add the "caosdb-f-entity-property" class to the form fields. Thus the + * fields are findable by the `getEntityXML` method which is used in + * `get_shortcut_entities` to generate the entity xml from the shortcut + * form. + * + * @param {HTMLElement} form - form which contains the fields where the + * class is to be added. + */ + this._toggle_entity_property_class = function(form) { + form.addEventListener("caosdb.form.ready", () => { + $(form).find(".caosdb-f-field").toggleClass("caosdb-f-entity-property", true); + }); + $(form).find(".caosdb-f-field").toggleClass("caosdb-f-entity-property", true); + } + this.make_create_fields = function(include) { return [ include, { - type: "text", name: query_shortcuts._shortcuts_property_description_name, required: true, + type: "text", name: query_shortcuts._shortcuts_property_description_name, label: "Description", required: true, cached: true, //help: query_shortcuts._description_help, TODO }, { - type: "text", name: query_shortcuts._shortcuts_property_query_name, required: true, + type: "text", name: query_shortcuts._shortcuts_property_query_name, label: "Query", required: true, cached: true, //help: query_shortcuts._query_help, TODO } @@ -933,6 +951,7 @@ var query_shortcuts = new function() { fields: this.make_update_fields(entity[0], olddef.attr("data-shortcut-description"), olddef.attr("data-query-string")), }; var form = form_elements.make_form(form_config); + this._toggle_entity_property_class(form); this.logger.trace("leave make_update_form", form); return form; diff --git a/src/core/js/tour.js b/src/core/js/tour.js index 57b426818abf2da0d58b46db3ce5bd893721bcc4..7b47dd37239c29b5c5e7edb67f1d16fe43bf8ad6 100644 --- a/src/core/js/tour.js +++ b/src/core/js/tour.js @@ -698,11 +698,14 @@ var tour = new function() { /** * Initialize the tour. + * + * The `refresh` argument is currently only used interactively on the debugging console. */ - this.init = async function _in(refresh=false) { + this.init = async function _in(refresh) { try { - tour.debug("initializing tour module"); - if (refresh) { + tour.debug("initializing tour module, refresh: " + refresh); + if (refresh === true) { + tour.info("Refreshing tour state."); localStorage.removeItem("tour_state"); } await tour.load_tour(); diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index b496c3b7a176828cf99a3efd017d591dde2e937b..df93703d83fafde15908efb40c3ae578824980d9 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -26,7 +26,7 @@ window.addEventListener('error', (e) => globalError(e.error)); -var globalError = function(error) { +var globalError = function (error) { var stack = error.stack; var message = "Error! Please help to make CaosDB better! Copy this message and send it via email to info@indiscale.com.\n\n"; message += error.toString(); @@ -40,7 +40,7 @@ var globalError = function(error) { throw error; } -var globalClassNames = new function() { +var globalClassNames = new function () { this.NotificationArea = "caosdb-preview-notification-area"; this.WaitingNotification = "caosdb-preview-waiting-notification"; this.ErrorNotification = "caosdb-preview-error-notification"; @@ -49,62 +49,192 @@ var globalClassNames = new function() { /** * navbar module contains convenience functions for the navbar. */ -this.navbar = new function() { +this.navbar = new function () { var logger = log.getLogger("navbar"); this.logger = logger; /** - * Add a button to the navbar (left navbar, append right), wrapped in a LI - * element. + * Add a button to the navbar. * * If the param `button` is a string then a suitable BUTTON element will be - * created. If the button is otherwise an HTMLElement it will just be - * wrapped and appended to the navbar. + * created. If the button is otherwise an HTMLElement or an array of + * HTMLElements it will just be wrapped, styled and appended to the navbar. * - * The optional `click_callback` function is bound to the click event. + * The optional `options` object knows the following optional keys: + * @property {function} callback - a function which will be bound to the + * click event of the button. + * @property {string} title - the title attribute (a kind of tooltip) + * of the button. + * @property {HTMLElement} menu - where to append the button. Defaults + * to the navbar itself. * - * @param {String|HTMLElement} button - add this button to navbar - * @param {function} [click_callback] - callback function for click + * If the `options["menu"] parameter is not set, the button is appended + * to the navbar directly and appears left of all previously appended + * children. + * + * + * @param {String|HTMLElement|HTMLElement[]} button - the button + * @param {object} [options] - further options * @return {HTMLElement} wrapper of the new button */ - this.add_button = function(button, click_callback) { + this.add_button = function (button, options) { + logger.trace("enter add_button", button, options); + + // assure that button parameter is {String|HTMLElement|HTMLElement[]} + if (typeof button === "undefined") { + throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], was " + typeof button); + } else if (Array.isArray(button)) { + for (const element of button) { + if (!(element instanceof HTMLElement)) { + throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], element was " + typeof element); + } + } + } else if (!(typeof button === "string" || button instanceof String || button instanceof HTMLElement)) { + throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], was " + typeof button); + } + + // default: empty options + const _options = options || {}; var button_elem = button; if (typeof button === "string" || button instanceof String) { // create button element from string button_elem = $('<button>' + button + '</button>'); } - $(button_elem).toggleClass("navbar-btn", true); - $(button_elem).toggleClass("btn", true); - $(button_elem).toggleClass("btn-link", true); + + // set title + const title = _options["title"]; + if (typeof _options !== "undefined") { + $(button_elem).attr("title", title); + } + // bind click - if(typeof click_callback === "function") { + const click_callback = _options["callback"] + if (typeof click_callback === "function") { $(button_elem).click(click_callback); } // wrapp button let wrapper = $("<li></li>").append(button_elem); - $('#top-navbar').find("ul.caosdb-navbar").first().append(wrapper); + + // menu defaults to the navbar + const menu = _options["menu"] || this.get_navbar(); + + if ($(menu).is("ul.caosdb-navbar")) { + // special styling for buttons which are added directly to the + // navbar + $(button_elem) + .toggleClass("navbar-btn", true) + .toggleClass("btn", true) + .toggleClass("btn-link", true); + } + + logger.debug("add", wrapper, "to", menu); + $(menu).append(wrapper); + + logger.trace("leave add_button", wrapper[0]); return wrapper[0]; } - this.init = function() { + this.init = function () { $("nav.navbar-fixed-top") - .on("shown.bs.collapse", function(e) { + .on("shown.bs.collapse", function (e) { logger.trace("navbar expands", e); }) - .on("hidden.bs.collapse", function(e) { + .on("hidden.bs.collapse", function (e) { logger.trace("navbar shrinks", e); }); } + + /** + * Create initially empty tool box dropdown and append to navbar. + * + * The returned element is the drop-down menu which will eventually contain + * the tool buttons. That means, the buttons can be added directly to the + * returned element to appear in the drop-down menu. + * + * @return {HTMLElement} the dropdown-menu. + */ + this.init_toolbox = function (name) { + var button = $(`<a class="dropdown-toggle" + data-toggle="dropdown" href="#">${name} + <span class="caret"></span></a>`)[0]; + + var menu = $(`<ul + class="caosdb-v-navbar-toolbox + caosdb-f-navbar-toolbox + dropdown-menu" + data-toolbox-name="${name}"/>`)[0]; + + const wrapper = this.add_button([button, menu]); + $(wrapper).toggleClass("dropdown", true) + .children() + .toggleClass("btn", false) + .toggleClass("btn-link", false) + .toggleClass("navbar-btn", false); + + return menu; + } + + /** + * Add a tool to a toolbox. + * + * If the button is a string a new button element is created showing the + * string as its label. Otherwise the button should be an HTMLElement which + * is directly added to the toolbox. + * toolbox. + * + * The `callback` is a function which will be bound to the click event of + * the button. + * + * The passed or created button is wrapped in a LI element and added to the + * toolbox. The wrapper of the button is returned + * + * The optional `options` object knows the following optional keys: + * @property {function} callback - a function which will be bound to the + * click event of the button. + * @property {string} title - the title attribute (a kind of tooltip) + * of the button. + * + * @param {string|HTMLElement} button + * @param {string} [toolbox] - the name of the toolbox + * @param {object} [options] further options. + * @return {HTMLElement} the button wrapper. + */ + this.add_tool = function (button, toolbox, options) { + const toolbox_element = this.get_toolbox(toolbox); + + // put toolbox_element as menu into the options for `add_button` + const _options = $.extend({ + menu: toolbox_element + }, options); + + const wrapper = this.add_button(button, _options); + return wrapper; + } + + this.get_navbar = function () { + return $('#top-navbar') + .find("ul.caosdb-navbar")[0]; + } + + this.get_toolbox = function (name) { + var toolbox = $(this.get_navbar()).find(".caosdb-f-navbar-toolbox[data-toolbox-name='" + name + "']"); + if (toolbox.length > 0) { + return toolbox[0]; + } else { + return this.init_toolbox(name); + } + } + } -this.caosdb_utils = new function() { - this.assert_string = function(obj, name, optional=false) { +this.caosdb_utils = new function () { + this.assert_string = function (obj, name, optional = false) { if (typeof obj === "undefined" && optional) { return obj; } @@ -114,7 +244,7 @@ this.caosdb_utils = new function() { throw new TypeError(name + " is expected to be a string, was " + typeof obj); } - this.assert_type = function(obj, type, name, optional=false) { + this.assert_type = function (obj, type, name, optional = false) { if (typeof obj === "undefined" && optional) { return obj; } @@ -124,14 +254,14 @@ this.caosdb_utils = new function() { return obj; } - this.assert_html_element = function(obj, name) { + this.assert_html_element = function (obj, name) { if (typeof obj === "undefined" || !(obj instanceof HTMLElement)) { throw new TypeError(name + " is expected to be an HTMLElement, was " + typeof obj); } return obj; } - this.assert_array = function(obj, name, wrap_if_not_array) { + this.assert_array = function (obj, name, wrap_if_not_array) { if (Array.isArray(obj)) { return obj; } else if (wrap_if_not_array) { @@ -144,8 +274,8 @@ this.caosdb_utils = new function() { /** * connection module contains all ajax calls. */ -this.connection = new function() { - this._init = function() { +this.connection = new function () { + this._init = function () { /** * Send a get request. */ @@ -224,8 +354,8 @@ this.connection = new function() { console.log(error); } else if (error.status != null) { throw new Error( - "POST scripting returned with HTTP status " + error.status - + " - " + error.statusText); + "POST scripting returned with HTTP status " + error.status + + " - " + error.statusText); } else { throw error; } @@ -284,7 +414,7 @@ this.connection = new function() { /** * Return the base path of the server. */ - this.getBasePath = function() { + this.getBasePath = function () { var base = window.location.origin + "/"; if (typeof window.sessionStorage.caosdbBasePath !== "undefined") { base = window.sessionStorage.caosdbBasePath; @@ -305,7 +435,7 @@ this.connection = new function() { * @param {string[]|number[]} ids * @return {string} the URI. */ - this.getEntityUri = function(ids) { + this.getEntityUri = function (ids) { return this.getBasePath() + transaction.generateEntitiesUri(ids); } } @@ -316,14 +446,14 @@ this.connection = new function() { * transformation module contains all code for tranforming xml into html via * xslt. */ -this.transformation = new function() { +this.transformation = new function () { /** * remove all permission information from a server's response. * * @param {XMLDocument} xml * @return {XMLDocument} without <Permissions> tags. */ - this.removePermissions = function(xml) { + this.removePermissions = function (xml) { $(xml).find('Permissions').remove(); return xml } @@ -424,10 +554,10 @@ this.transformation = new function() { * be included. * @return {XMLDocument} a new style sheets with all template rules; */ - this.mergeXsltScripts = function(xslMain, xslIncludes) { + this.mergeXsltScripts = function (xslMain, xslIncludes) { let ret = getXSLScriptClone(xslMain); for (var i = 0; i < xslIncludes.length; i++) { - $(xslIncludes[i].firstElementChild).find('xsl\\:template').each(function(index) { + $(xslIncludes[i].firstElementChild).find('xsl\\:template').each(function (index) { $(ret.firstElementChild).append(this); }); } @@ -439,7 +569,7 @@ this.transformation = new function() { * transaction module contains all code for insertion, update and deletion of * entities. Currently, only updates are implemented. */ -this.transaction = new function() { +this.transaction = new function () { this.classNameUpdateForm = "caosdb-update-entity-form"; /** @@ -499,14 +629,14 @@ this.transaction = new function() { * @param {String[]} entityIds - An array of entity ids.. * @return {String} The uri. */ - this.generateEntitiesUri = function(entityIds) { + this.generateEntitiesUri = function (entityIds) { return "Entity/" + entityIds.join("&"); } /** * Submodule for update transactions. */ - this.update = new function() { + this.update = new function () { /** * Create a form for updating entities. It has only a textarea and a * submit button. @@ -520,7 +650,7 @@ this.transaction = new function() { * @param {String} entityXmlStr, the old entity * @param {function} putCallback, the function which sends a put request. */ - this.createUpdateForm = function(entityXmlStr, putCallback) { + this.createUpdateForm = function (entityXmlStr, putCallback) { // check the parameters if (putCallback == null) { throw new Error("putCallback function must not be null."); @@ -544,13 +674,13 @@ this.transaction = new function() { form.append(resetButton); // reset restores the original xmlStr - form.on('reset', function(e) { + form.on('reset', function (e) { textarea.find('textarea').val(entityXmlStr); return false; }); // submit calls the putCallback - form.submit(function(e) { + form.submit(function (e) { putCallback(e.target.updateXml.value); return false; }); @@ -568,7 +698,7 @@ this.transaction = new function() { * @param {HTMLElement} entity, the div which represent the entity. * @return {Object} a state machine. */ - this.updateSingleEntity = function(entity) { + this.updateSingleEntity = function (entity) { let updatePanel = transaction.update.createUpdateEntityPanel(transaction.update.createUpdateEntityHeading($(entity).find('.caosdb-entity-panel-heading')[0])); var app = new StateMachine({ transitions: [{ @@ -593,14 +723,14 @@ this.transaction = new function() { to: 'final' }, ], }); - app.errorHandler = function(fn) { + app.errorHandler = function (fn) { try { fn(); } catch (e) { setTimeout(() => app.resetApp(e), 1000); } } - app.onInit = function(e, entity) { + app.onInit = function (e, entity) { // remove entity $(entity).hide(); app.errorHandler(() => { @@ -617,7 +747,7 @@ this.transaction = new function() { }); // retrieve old xml, trigger state change when response is ready }; - app.onOpenForm = function(e, entityXmlStr) { + app.onOpenForm = function (e, entityXmlStr) { app.errorHandler(() => { // create and show Form let form = transaction.update.createUpdateForm(entityXmlStr, (xmlstr) => { @@ -626,14 +756,14 @@ this.transaction = new function() { updatePanel.append(form); }); }; - app.onResetApp = function(e, error) { + app.onResetApp = function (e, error) { $(entity).show(); $(updatePanel).remove(); if (error != null) { globalError(error); } }; - app.onShowUpdatedEntity = function(e, newentity) { + app.onShowUpdatedEntity = function (e, newentity) { // remove updatePanel updatePanel.remove(); // show new version of entity @@ -641,7 +771,7 @@ this.transaction = new function() { // remove old version $(entity).remove(); }; - app.onSubmitForm = function(e, xmlstr) { + app.onSubmitForm = function (e, xmlstr) { // remove form $(updatePanel).find('form').remove(); @@ -673,7 +803,7 @@ this.transaction = new function() { ); }); }; - app.onLeaveWaitPutEntity = function() { + app.onLeaveWaitPutEntity = function () { // remove waiting notifications removeAllWaitingNotifications(updatePanel); }; @@ -700,19 +830,19 @@ this.transaction = new function() { return xml2str(transformation.removePermissions(xml)); } - this.createWaitRetrieveNotification = function() { + this.createWaitRetrieveNotification = function () { return createWaitingNotification("Retrieving xml and loading form. Please wait."); } - this.createWaitUpdateNotification = function() { + this.createWaitUpdateNotification = function () { return createWaitingNotification("Sending update to the server. Please wait."); } - this.createErrorInUpdatedEntityNotification = function() { + this.createErrorInUpdatedEntityNotification = function () { return createErrorNotification("The update was not successful."); } - this.addErrorNotification = function(elem, err) { + this.addErrorNotification = function (elem, err) { $(elem).append(err); return elem; } @@ -724,7 +854,7 @@ this.transaction = new function() { * @param {HTMLElement} heading, the heading of the panel. * @return {HTMLElement} A div. */ - this.createUpdateEntityPanel = function(heading) { + this.createUpdateEntityPanel = function (heading) { let panel = $('<div class="panel panel-default" style="border-color: blue;"/>'); panel.append(heading); return panel[0]; @@ -739,7 +869,7 @@ this.transaction = new function() { * @param {HTMLElement} entityHeading, the heading of the entity. * @return {HTMLElement} the heading for the update panel. */ - this.createUpdateEntityHeading = function(entityHeading) { + this.createUpdateEntityHeading = function (entityHeading) { let heading = entityHeading.cloneNode(true); let update = $('<span><h3>Update</h3></span>')[0]; $(heading).children().slice(1).remove(); @@ -754,19 +884,19 @@ this.transaction = new function() { * @param {HTMLElement} entityPanel, the entity panel. * @return {HTMLElement} the heading. */ - this.getEntityHeading = function(entityPanel) { + this.getEntityHeading = function (entityPanel) { return $(entityPanel).find('.caosdb-entity-panel-heading')[0]; } - this.initUpdate = function(button) { + this.initUpdate = function (button) { transaction.update.updateSingleEntity( $(button).closest('.caosdb-entity-panel')[0] ); } - this.createCloseButton = function(close, callback) { + this.createCloseButton = function (close, callback) { let button = $('<button title="Cancel update" class="btn btn-link close" aria-label="Cancel update">×</button>'); - button.bind('click', function() { + button.bind('click', function () { $(this).closest(close).hide(); callback(); }); @@ -776,7 +906,7 @@ this.transaction = new function() { } -var paging = new function() { +var paging = new function () { this.defaultPageLen = 10; /** @@ -791,7 +921,7 @@ var paging = new function() { * the number of entities which are currently shown on this * page. */ - this.initPaging = function(href, totalEntities) { + this.initPaging = function (href, totalEntities) { if (totalEntities == null) { return false; @@ -841,7 +971,7 @@ var paging = new function() { * the page the new uri shall point to * @return a string uri which points to the page denotes by the parameter */ - this.getPageHref = function(uri_old, page) { + this.getPageHref = function (uri_old, page) { if (uri_old == null) { throw new Error("uri was null."); } @@ -869,7 +999,7 @@ var paging = new function() { * @param uri An arbitrary URI, usually the current URI of the window. * @return The first part of the query segment which begins with 'P=' */ - this.getPSegmentFromUri = function(uri) { + this.getPSegmentFromUri = function (uri) { if (uri == null) { throw new Error("uri was null."); } @@ -886,7 +1016,7 @@ var paging = new function() { * a paging string * @return a String */ - this.getPrevPage = function(P) { + this.getPrevPage = function (P) { if (P == null) { throw new Error("P was null"); } @@ -920,7 +1050,7 @@ var paging = new function() { * total numbers of entities. * @return a String */ - this.getNextPage = function(P, n) { + this.getNextPage = function (P, n) { // check n and P for null values and correct formatting if (n == null) { throw new Error("n was null"); @@ -950,8 +1080,8 @@ var paging = new function() { } }; -var queryForm = new function() { - this.init = function(form) { +var queryForm = new function () { + this.init = function (form) { this.restoreLastQuery(form, () => window.sessionStorage.lastQuery); this.bindOnClick(form, (set) => { window.sessionStorage.lastQuery = set; @@ -959,7 +1089,7 @@ var queryForm = new function() { }); }; - this.restoreLastQuery = function(form, getter) { + this.restoreLastQuery = function (form, getter) { if (form == null) { throw new Error("form was null"); } @@ -972,16 +1102,16 @@ var queryForm = new function() { * @value {string} query - the query string. * @param {string} paging - the paging string, e.g. 0L10. */ - this.redirect = function(query, paging) { + this.redirect = function (query, paging) { var pagingparam = "" - if(paging && paging.length > 0) { + if (paging && paging.length > 0) { pagingparam = "P=" + paging + "&"; } location.href = connection.getBasePath() + "Entity/?" + pagingparam + "query=" + query; } - this.bindOnClick = function(form, setter) { - if (setter == null || typeof(setter) !== 'function' || setter.length !== 1) { + this.bindOnClick = function (form, setter) { + if (setter == null || typeof (setter) !== 'function' || setter.length !== 1) { throw new Error("setter must be a function with one param"); } @@ -990,7 +1120,7 @@ var queryForm = new function() { and the click handler of the button. See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary. */ - var submithandler = function() { + var submithandler = function () { // store current query var queryField = form.query; @@ -1004,7 +1134,7 @@ var queryForm = new function() { setter(queryField.value); var paging = ""; - if(form.P && !queryForm.isSelectQuery(queryField.value)) { + if (form.P && !queryForm.isSelectQuery(queryField.value)) { paging = form.P.value } @@ -1013,15 +1143,15 @@ var queryForm = new function() { // handler for the form - form.onsubmit = function(e) { - e.preventDefault(); - submithandler(); + form.onsubmit = function (e) { + e.preventDefault(); + submithandler(); return false; - }; + }; // same handler for the button - form.getElementsByClassName("caosdb-search-btn")[0].onclick = function() { - submithandler(); + form.getElementsByClassName("caosdb-search-btn")[0].onclick = function () { + submithandler(); }; }; @@ -1031,7 +1161,7 @@ var queryForm = new function() { * @param {HTMLElement} query, the query to be tested. * @return {Boolean} */ - this.isSelectQuery = function(query) { + this.isSelectQuery = function (query) { return query.toUpperCase().startsWith("SELECT"); } @@ -1042,7 +1172,7 @@ var queryForm = new function() { * @param {HTMLElement} form, the query form. * @return {HTMLElement} the form without the paging input. */ - this.removePagingField = function(form) { + this.removePagingField = function (form) { $(form.P).remove(); return form; } @@ -1052,9 +1182,9 @@ var queryForm = new function() { /** * Small module containing only a converter from markdown to html. */ -this.markdown = new function() { +this.markdown = new function () { this.dependencies = ["showdown", "caosdb_utils"]; - this.init = function() { + this.init = function () { this.converter = new showdown.Converter(); }; @@ -1065,7 +1195,7 @@ this.markdown = new function() { * @param {HTMLElement} textElement - an element with text which is to be * converted to html. */ - this.toHtml = function(textElement) { + this.toHtml = function (textElement) { let text = $(textElement).html(); let html = this.textToHtml(text); $(textElement).html(html); @@ -1074,28 +1204,28 @@ this.markdown = new function() { /** * Convert a markdown text to HTML */ - this.textToHtml = function(text) { + this.textToHtml = function (text) { caosdb_utils.assert_string(text, "param `text`"); return this.converter.makeHtml(text.trim()); }; - $(document).ready(function() { + $(document).ready(function () { caosdb_modules.register(markdown); }); } -var hintMessages = new function() { - this.init = function() { +var hintMessages = new function () { + this.init = function () { for (var entity of $('.caosdb-entity-panel')) { this.hintMessages(entity); } } - this.removeMessages = function(entity) { + this.removeMessages = function (entity) { $(entity).find(".alert").remove(); } - this.unhintMessages = function(entity) { + this.unhintMessages = function (entity) { $(entity).find(".caosdb-f-message-badge").remove(); $(entity).find(".alert").show(); } @@ -1115,7 +1245,7 @@ var hintMessages = new function() { * @param {HTMLElement} entity - the element where to replace the * messages. */ - this.hintMessages = function(entity) { + 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 @@ -1131,7 +1261,7 @@ var hintMessages = new function() { for (let alrt in messageType) { // find all message divs - $(entity).find(".alert.alert-" + alrt).each(function(index) { + $(entity).find(".alert.alert-" + alrt).each(function (index) { var messageElem = $(this); // this way only one badge is shown, even if there are more @@ -1140,7 +1270,7 @@ var hintMessages = new function() { // 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) { + messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).on("click", function (e) { // TODO use remove here instead of hide? $(this).hide(); @@ -1235,7 +1365,7 @@ function postXml(xml, basepath, querySegment, timeout) { dataType: 'xml', timeout: timeout, statusCode: { - 401: function() { + 401: function () { throw new Error("unauthorized"); }, }, @@ -1273,7 +1403,7 @@ async function load_config(filename) { } else if (error.status == 404) { return []; } else { - throw new Error("loading '"+ uri + "' failed.", error); + throw new Error("loading '" + uri + "' failed.", error); } } return data; @@ -1391,13 +1521,13 @@ function initOnDocumentReady() { } // show image 100% width - $(".entity-image-preview").click(function() { + $(".entity-image-preview").click(function () { $(this).css('width', '100%'); $(this).css('max-width', ""); $(this).css('max-height', ""); }); - if(typeof caosdb_modules.auto_init === "undefined") { + if (typeof caosdb_modules.auto_init === "undefined") { // the test index.html sets this to false, // unset -> no tests caosdb_modules.auto_init = true; @@ -1429,12 +1559,12 @@ class _CaosDBModules { * @throws TypeError - if module has no `init` method. */ register(module) { - if(!(typeof module.init === "function")) { + if (!(typeof module.init === "function")) { throw new TypeError("modules must define an init function"); } this.modules.push(module); - if(this.auto_init) { + if (this.auto_init) { this._init_module(module); } } diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index 6b709d0e25647807d852bf9d6b778b54ff543d08..299b1dac37a1029e5bc7c48008efa3735d61037b 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -46,9 +46,9 @@ <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath, '?P=0L10&query=FIND+Entity+which+references+', current())"/> </xsl:attribute> - <span class="glyphicon glyphicon-share-alt flipped-horiz-icon"/> References + <span class="glyphicon glyphicon-share-alt flipped-horiz-icon"/> + <span class="hidden-xs"> References</span> </a> - <span class="spacer"/> </xsl:template> <!-- special entity properties like type, checksum, path... --> <xsl:template match="@datatype" mode="entity-heading-attributes-datatype"> @@ -87,11 +87,16 @@ </p> </xsl:template> <xsl:template match="*" mode="entity-action-panel"> - <div class="caosdb-entity-actions-panel text-right btn-group-xs"></div> + <div class="caosdb-entity-actions-panel text-right btn-group-xs"> + <xsl:apply-templates select="Version/Successor" mode="entity-action-panel-version"> + <xsl:with-param name="entityId" select="@id"/> + </xsl:apply-templates> + </div> </xsl:template> <!-- Main entry for ENTITIES --> <xsl:template match="Property|Record|RecordType|File" mode="entities"> <div class="panel panel-default caosdb-entity-panel"> + <xsl:apply-templates select="Version" mode="entity-version-marker"/> <xsl:attribute name="id"> <xsl:value-of select="@id"/> </xsl:attribute> @@ -138,17 +143,22 @@ </h5> </div> <div class="col-sm-4 text-right"> - <h5> + <h5 class="caosdb-v-entity-header-buttons-list"> <!-- Button for expanding/collapsing the comments section--> - <span class="caosdb-clickable glyphicon glyphicon-comment" data-toggle="collapse" title="Comments" style="margin-right: 10px;"> + <span class="caosdb-clickable glyphicon glyphicon-comment" data-toggle="collapse" title="Toggle the comments section at the bottom of this entity."> <xsl:attribute name="data-target"> <xsl:value-of select="concat('#', 'comment_', $entityid)"/> </xsl:attribute> </span> + <span> <xsl:apply-templates mode="backreference-link" select="@id"/> + </span> <span class="label caosdb-id caosdb-id-button hidden"> <xsl:value-of select="@id"/> </span> + <xsl:apply-templates mode="entity-heading-attributes-version" select="Version"> + <xsl:with-param name="entityId" select="@id"/> + </xsl:apply-templates> </h5> </div> </div> @@ -285,6 +295,9 @@ <xsl:otherwise> <xsl:element name="span"> <xsl:attribute name="class"> + <!-- DEPRECATED css class .caosdb-property-text-value - Use + .caosdb-f-property-single-raw-value or introduce new + .caosdb-v-property-text-value --> <xsl:value-of select="'caosdb-f-property-single-raw-value caosdb-property-text-value'"/> </xsl:attribute> <xsl:call-template name="trim"> @@ -297,6 +310,9 @@ </xsl:choose> </xsl:when> <xsl:otherwise> + <!-- DEPRECATED css class .caosdb-property-text-value - Use + .caosdb-f-property-single-raw-value or introduce new + .caosdb-v-property-text-value --> <span class="caosdb-f-property-single-raw-value caosdb-property-text-value"/> </xsl:otherwise> </xsl:choose> @@ -318,6 +334,19 @@ </xsl:with-param> </xsl:call-template> </xsl:for-each> + <xsl:for-each select="Record|RecordType|File|Property"> + <xsl:call-template name="single-value"> + <xsl:with-param name="reference"> + <xsl:value-of select="'true'"/> + </xsl:with-param> + <xsl:with-param name="value"> + <xsl:value-of select="@id"/> + </xsl:with-param> + <xsl:with-param name="boolean"> + <xsl:value-of select="'false'"/> + </xsl:with-param> + </xsl:call-template> + </xsl:for-each> </xsl:element> </div> </xsl:template> @@ -351,7 +380,7 @@ <xsl:when test="contains(concat('<',@datatype),'<LIST<')"> <!-- list --> <xsl:choose> - <xsl:when test="translate(normalize-space(text()),'0123456789','')='' and not(contains('+LIST<INTEGER>+LIST<DOUBLE>+LIST<TEXT>+LIST<BOOLEAN>+LIST<DATETIME>+',concat('+',@datatype,'+')))"> + <xsl:when test="not(contains('+LIST<INTEGER>+LIST<DOUBLE>+LIST<TEXT>+LIST<BOOLEAN>+LIST<DATETIME>+',concat('+',@datatype,'+')))"> <xsl:apply-templates mode="property-reference-value-list" select="."/> </xsl:when> <xsl:otherwise> @@ -361,17 +390,38 @@ </xsl:when> <!-- hence, this is no collection --> <xsl:otherwise> - <xsl:call-template name="single-value"> - <xsl:with-param name="value"> - <xsl:value-of select="text()"/> - </xsl:with-param> - <xsl:with-param name="reference"> - <xsl:value-of select="translate(normalize-space(text()),'0123456789','')='' and not(contains('+INTEGER+DOUBLE+TEXT+BOOLEAN+DATETIME+',concat('+',@datatype,'+')))"/> - </xsl:with-param> - <xsl:with-param name="boolean"> - <xsl:value-of select="@datatype='BOOLEAN'"/> - </xsl:with-param> - </xsl:call-template> + <xsl:choose> + <!-- the referenced entities have been returned. --> + <xsl:when test="*[@id]"> + <xsl:for-each select="*[@id]"> + <xsl:call-template name="single-value"> + <xsl:with-param name="reference"> + <xsl:value-of select="'true'"/> + </xsl:with-param> + <xsl:with-param name="value"> + <xsl:value-of select="@id"/> + </xsl:with-param> + <xsl:with-param name="boolean"> + <xsl:value-of select="'false'"/> + </xsl:with-param> + </xsl:call-template> + </xsl:for-each> + </xsl:when> + <xsl:otherwise> + <!-- only the ids are available --> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="text()"/> + </xsl:with-param> + <xsl:with-param name="reference"> + <xsl:value-of select="not(contains('+INTEGER+DOUBLE+TEXT+BOOLEAN+DATETIME+',concat('+',@datatype,'+')))"/> + </xsl:with-param> + <xsl:with-param name="boolean"> + <xsl:value-of select="@datatype='BOOLEAN'"/> + </xsl:with-param> + </xsl:call-template> + </xsl:otherwise> + </xsl:choose> </xsl:otherwise> </xsl:choose> <!-- unit --> @@ -452,4 +502,115 @@ </li> </ul> </xsl:template> + <!--VERSIONING--> + <xsl:template match="Version" mode="entity-heading-attributes-version"> + <xsl:param name="entityId"/> + <xsl:param name="versionModalId">version-modal-<xsl:value-of select="generate-id()"/></xsl:param> + <!-- the clock button which opens the window with the versioning info --> + <button title="Versioning Info" type="button" data-toggle="modal"> + <xsl:attribute name="data-target">#<xsl:value-of select="$versionModalId"/></xsl:attribute> + <xsl:attribute name="class"> + caosdb-f-entity-version-button caosdb-v-entity-version-button btn + <xsl:if test="Successor"> + <!-- indicate old version by color and symbol --> + <xsl:value-of select="' text-danger'"/> + </xsl:if> + </xsl:attribute> + <span class="glyphicon glyphicon-time"/> + </button> + <!-- the following div.modal is the window that pops up when the user clicks on the clock button --> + <div class="caosdb-f-entity-version-info modal fade" tabindex="-1" role="dialog"> + <xsl:attribute name="id"><xsl:value-of select="$versionModalId"/></xsl:attribute> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content text-left"> + <div> + <xsl:attribute name="class"> + modal-header + <xsl:if test="Successor"> + <!-- indicate old version by color --> + <xsl:value-of select="' bg-danger'"/> + </xsl:if> + </xsl:attribute> + <button type="button" class="close" data-dismiss="modal" aria-label="Close" title="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title">Version Info</h4> + <p class="caosdb-entity-heading-attr"> + <em class="caosdb-entity-heading-attr-name"> + This is + <xsl:if test="Successor"><b>not</b></xsl:if> + the latest version of this entity. + </em> + </p> + </div> + <div class="modal-body"> + <xsl:apply-templates mode="entity-version-modal-head" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + <p class="caosdb-entity-heading-attr"> + <em class="caosdb-entity-heading-attr-name">This version:</em> + <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) + </p> + <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + </div> + </div> + </div> + </div> + </xsl:template> + <xsl:template match="Predecessor" mode="entity-version-modal-predecessor"> + <!-- content of the versioning window --> + <xsl:param name="entityId"/> + <p class="caosdb-entity-heading-attr"> + <em class="caosdb-entity-heading-attr-name">Previous version:</em> + <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> + <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) + </a> + </p> + </xsl:template> + <xsl:template match="Successor" mode="entity-version-modal-head"> + <!-- content of the versioning window --> + <xsl:param name="entityId"/> + <p class="caosdb-entity-heading-attr"> + <em class="caosdb-entity-heading-attr-name">Newest version:</em> + <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute> + <xsl:value-of select="$entityId"/>@HEAD + </a> + </p> + </xsl:template> + <xsl:template match="Successor" mode="entity-version-modal-successor"> + <!-- content of the versioning window --> + <xsl:param name="entityId"/> + <p class="caosdb-entity-heading-attr"> + <em class="caosdb-entity-heading-attr-name">Next version:</em> + <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> + <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) + </a> + </p> + </xsl:template> + <xsl:template match="Version/Successor" mode="entity-action-panel-version"> + <!-- clickable warning message in the entity actions panel when there exists a newer version --> + <xsl:param name="entityId"/> + <a class="caosdb-f-entity-version-old-warning alert-warning btn btn-link" title="Go to the latest version of this entity."> + <xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute> + <strong>Warning</strong> A newer version exists! + </a> + </xsl:template> + <xsl:template match="Version" mode="entity-version-marker"> + <!-- content of the data-version-id attribute --> + <xsl:attribute name="data-version-id"> + <xsl:value-of select="@id"/> + </xsl:attribute> + <xsl:apply-templates select="Successor" mode="entity-version-marker"/> + </xsl:template> + <xsl:template match="Successor" mode="entity-version-marker"> + <!-- content of the data-version-successor attribute + This data-attribute marks entities which have a newer version. + --> + <xsl:attribute name="data-version-successor"> + <xsl:value-of select="@id"/> + </xsl:attribute> + </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/entity_palette.xsl b/src/core/xsl/entity_palette.xsl index aea42a8750a74f509557bcf842e4d1f8dc97bef9..961a51dc51584c3fe87496e54f9837fcec9de0b0 100644 --- a/src/core/xsl/entity_palette.xsl +++ b/src/core/xsl/entity_palette.xsl @@ -51,9 +51,22 @@ </xsl:template> <xsl:template match="Property"> - <li class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> - <xsl:attribute name="id">caosdb-f-edit-p-<xsl:value-of select="@id"/></xsl:attribute> - <xsl:value-of select="@name"/> - </li> + <xsl:choose> + <xsl:when test="@name='name'"> + <!-- ignore name property --> + </xsl:when> + <xsl:when test="@name='description'"> + <!-- ignore description property --> + </xsl:when> + <xsl:when test="@name='unit'"> + <!-- ignore unit property --> + </xsl:when> + <xsl:otherwise> + <li class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> + <xsl:attribute name="id">caosdb-f-edit-p-<xsl:value-of select="@id"/></xsl:attribute> + <xsl:value-of select="@name"/> + </li> + </xsl:otherwise> + </xsl:choose> </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl index f4a128f63f52488657b32e8ff03043c276d4aaa3..efbf2e6b6e4db0ad7b14cb1c2483157bc79001f1 100644 --- a/src/core/xsl/main.xsl +++ b/src/core/xsl/main.xsl @@ -180,6 +180,11 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/query_shortcuts.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_jupyterdrag.js')"/> + </xsl:attribute> + </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/annotation.js')"/> @@ -240,6 +245,16 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_revisions.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_sss_markdown.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_trigger_crawler_form.js')"/> + </xsl:attribute> + </xsl:element> <!--JS_EXTENSIONS--> </xsl:template> <xsl:template name="caosdb-data-container"> diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl index 8b6ae132b7b98118b03882c6bdeec259019c190b..be49ed7d889e920cfee87e723c4c5c3b8efb27b2 100644 --- a/src/core/xsl/query.xsl +++ b/src/core/xsl/query.xsl @@ -169,6 +169,7 @@ </span> </a> </xsl:template> + <xsl:template name="select-table-row"> <xsl:param name="entity-id"/> <tr> @@ -180,7 +181,7 @@ <xsl:with-param name="entity-id" select="$entity-id"/> </xsl:call-template> </td> - <xsl:for-each select="/Response/Query/Selection/Selector[@name!='id']"> + <xsl:for-each select="/Response/Query/Selection/Selector"> <xsl:call-template name="select-table-cell"> <xsl:with-param name="entity-id" select="$entity-id"/> <xsl:with-param name="field-name" select="translate(@name, $uppercase, $lowercase)"/> @@ -188,6 +189,7 @@ </xsl:for-each> </tr> </xsl:template> + <xsl:template name="select-table-cell"> <xsl:param name="entity-id"/> <xsl:param name="field-name"/> @@ -196,18 +198,90 @@ <xsl:value-of select="$field-name"/> </xsl:attribute> <div class="caosdb-f-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]"/> - </xsl:when> - <xsl:when test="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"> - <xsl:apply-templates mode="property-value" select="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"/> - </xsl:when> - </xsl:choose> + <xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments"> + <xsl:with-param name="first-segment"> + <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/> + </xsl:with-param> + <xsl:with-param name="next-segments"> + <xsl:value-of select="substring-after($field-name, '.')"/> + </xsl:with-param> + </xsl:apply-templates> </div> </td> </xsl:template> + + <xsl:template match="Property" mode="walk-select-segments"> + <!-- handle properties --> + <xsl:param name="next-segments"/> + + <xsl:choose> + <xsl:when test="$next-segments='value'"> + <!--handle value--> + <xsl:apply-templates mode="property-value" select="."/> + </xsl:when> + + <xsl:when test="translate($next-segments, $uppercase, $lowercase)='unit'"> + <!--handle unit--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="@unit"/> + </xsl:with-param> + </xsl:call-template> + </xsl:when> + + <xsl:when test="$next-segments!=''"> + <!--walk to next level of nested entities--> + <xsl:apply-templates select="*[@id]" mode="walk-select-segments"> + <xsl:with-param name="first-segment"> + <xsl:value-of select="substring-before(concat($next-segments, '.'), '.')"/> + </xsl:with-param> + <xsl:with-param name="next-segments"> + <xsl:value-of select="substring-after($next-segments, '.')"/> + </xsl:with-param> + </xsl:apply-templates> + </xsl:when> + + <xsl:otherwise> + <!--next is empty. handle complete property--> + <xsl:apply-templates mode="property-value" select="."/> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + <xsl:template match="*" mode="walk-select-segments"> + <!-- handle anything but attributes and properties --> + <xsl:param name="first-segment"/> + <xsl:param name="next-segments"/> + + <xsl:choose> + <xsl:when test="@*[translate($first-segment, $uppercase, $lowercase)=name()]"> + <!--handle attributes--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="@*[translate(name(), $uppercase, $lowercase)=$first-segment]"/> + </xsl:with-param> + </xsl:call-template> + </xsl:when> + + <xsl:when test="$next-segments"> + <!-- when there is a next-segmenst --> + <xsl:apply-templates select="Property[translate(@name, $uppercase, $lowercase)=$first-segment]" mode="walk-select-segments"> + <xsl:with-param name="next-segments"> + <xsl:value-of select="$next-segments"/> + </xsl:with-param> + </xsl:apply-templates> + </xsl:when> + + + <xsl:otherwise> + <!-- otherwise, this is the final segment and the reference can be printed. --> + <xsl:value-of select="@id"/> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + <xsl:template name="caosdb-query-panel"> + <!-- query panel, this is the area which contains the query form and other related stuff (e.g. query short cuts). --> <div class="container caosdb-query-panel"> <form class="panel" id="caosdb-query-form" method="GET"> <xsl:attribute name="action"> diff --git a/test/core/html/form_elements_example_1.html b/test/core/html/form_elements_example_1.html index aa10ac557cb9e015d08490bf8047a0e79a244407..9977e5d91c02cc0e4dd08e9f0939c85f98c6c1be 100644 --- a/test/core/html/form_elements_example_1.html +++ b/test/core/html/form_elements_example_1.html @@ -1,4 +1,7 @@ <div class="caosdb-f-form-wrapper"> + <!-- DEPRECATED css class .caosdb-property-text-value - Use + .caosdb-f-property-single-raw-value or introduce new + .caosdb-v-property-text-value --> <form action="#" class="form-horizontal" method="post" name="sample_creation.py"> <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> diff --git a/test/core/index.html b/test/core/index.html index 0d97a415334fd441319eec7e4db262c34d64ef07..50d8cbef8003d6fb9ab383f94405bcfa07270774 100644 --- a/test/core/index.html +++ b/test/core/index.html @@ -67,6 +67,8 @@ <script src="js/ext_bottom_line.js"></script> <script src="js/ext_revisions.js"></script> <script src="js/autocomplete.js"></script> + <script src="js/ext_sss_markdown.js"></script> + <script src="js/ext_trigger_crawler_form.js"></script> <!--EXTENSIONS--> <script src="js/modules/webcaosdb.js.js"></script> <script src="js/modules/caosdb.js.js"></script> @@ -85,5 +87,7 @@ <script src="js/modules/ext_bottom_line.js.js"></script> <script src="js/modules/ext_revisions.js.js"></script> <script src="js/modules/autocomplete.js.js"></script> + <script src="js/modules/ext_sss_markdown.js.js"></script> + <script src="js/modules/ext_trigger_crawler_form.js.js"></script> </body> </html> diff --git a/test/core/js/modules/caosdb.js.js b/test/core/js/modules/caosdb.js.js index 35a1834083d0b849a62b5f9552a3b7f1259c56c4..76141117ffc845917c0e5ff50a165e7718663a31 100644 --- a/test/core/js/modules/caosdb.js.js +++ b/test/core/js/modules/caosdb.js.js @@ -167,6 +167,17 @@ QUnit.test("getProperties", function(assert) { assert.equal(ps[0].datatype, "TEXT"); }); +QUnit.test("getEntityIdVersion", function(assert) { + // without version + var html = $('<div data-entity-id="1234"/>')[0]; + assert.equal(getEntityIdVersion(html), "1234", "id extracted"); + + // with version + html = $('<div data-entity-id="1234" data-version-id="abcd"/>')[0]; + assert.equal(getEntityIdVersion(html), "1234@abcd", "<id>@<version> extracted"); + +}); + /** * @author Alexander Schlemmer * Test whether parents are retrieved correctly. @@ -442,6 +453,17 @@ QUnit.test("getEntityRole", function(assert) { }); +QUnit.test("getEntityUnit", function(assert) { + var property1 = $(`<div><p class="caosdb-entity-heading-attr"><em + class="caosdb-entity-heading-attr-name">unit:</em>m</p></div>`); + assert.equal(getEntityUnit(property1[0]), "m"); + + var property2 = $(`<div><input type="text" class="caosdb-f-entity-unit" + value="m/s"/><div>`); + assert.equal(getEntityUnit(property2[0]), "m/s"); +}); + + // Test for bug #53 // https://gitlab.com/caosdb/caosdb-webui/issues/53 QUnit.test("unset_entity_references", function(assert) { diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index c6326e4400750f75ef0b939990a941c2f706da2e..ae11b04a380f162018de70a53409e34b4e6990c6 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -24,12 +24,10 @@ /* SETUP ext_references module */ QUnit.module("edit_mode.js", { - before: function(assert) { + before: function (assert) { + this.form_elements_query = form_elements._query; + this.edit_mode_query = edit_mode.query; var done = assert.async(); - // overwrite query - edit_mode.query = async function(str) { - return []; - } retrieveTestEntities("edit_mode/getProperties_1.xml").then(entities => { this.testEntity_getProperties_1 = entities[0]; this.testEntity_make_property_editable_1 = entities[0]; @@ -39,9 +37,14 @@ QUnit.module("edit_mode.js", { done(); }); }, - after: function(assert) { + after: function (assert) { $('.modal.fade').has(".dropzone").remove(); - } + }, + afterEach: function (assert) { + // remove mock up functions + edit_mode.query = this.edit_mode_query; + form_elements._query = this.form_elements_query; + }, }); /** @@ -50,28 +53,28 @@ QUnit.module("edit_mode.js", { * * @return {HTMLElement[]} */ -async function retrieveTestEntities(testCase, transform=true) { - entities = await connection.get("xml/"+ testCase); +async function retrieveTestEntities(testCase, transform = true) { + entities = await connection.get("xml/" + testCase); return transformation.transformEntities(entities); } -QUnit.test("available", function(assert) { +QUnit.test("available", function (assert) { assert.ok(edit_mode); }); -QUnit.test("init", function(assert){ +QUnit.test("init", function (assert) { assert.ok(edit_mode.init); }); -QUnit.test("dragstart", function(assert){ +QUnit.test("dragstart", function (assert) { assert.ok(edit_mode.dragstart); }); -QUnit.test("dragleave", function(assert){ +QUnit.test("dragleave", function (assert) { assert.ok(edit_mode.dragleave); }); -QUnit.test("dragover", function(assert){ +QUnit.test("dragover", function (assert) { assert.ok(edit_mode.dragover); }); @@ -85,7 +88,7 @@ function assert_throws(assert, cb, message, name, info) { } } -QUnit.test("add_new_property", function(assert){ +QUnit.test("add_new_property", function (assert) { assert.ok(edit_mode.add_new_property); var done = assert.async(2); @@ -111,7 +114,11 @@ QUnit.test("add_new_property", function(assert){ // test good cases assert.equal($(entity).find("#test_new_prop").length, 0, "no property"); - entity.addEventListener("caosdb.edit_mode.property_added", function(e){assert.ok(e.target === new_prop, "event fired on newprop"); assert.ok(this === entity, "event detected on entity"); done();}, true); + entity.addEventListener("caosdb.edit_mode.property_added", function (e) { + assert.ok(e.target === new_prop, "event fired on newprop"); + assert.ok(this === entity, "event detected on entity"); + done(); + }, true); edit_mode.add_new_property(entity, new_prop, (x) => { assert.ok(x === new_prop, "make_property_editable_cb called"); @@ -124,56 +131,56 @@ QUnit.test("add_new_property", function(assert){ }); -QUnit.test("property_added", function(assert){ +QUnit.test("property_added", function (assert) { assert.ok(edit_mode.property_added, "available"); assert.ok(edit_mode.property_added instanceof Event, "is event"); }); -QUnit.test("add_dropped_property", function(assert){ +QUnit.test("add_dropped_property", function (assert) { assert.ok(edit_mode.add_dropped_property); }); -QUnit.test("add_dropped_parent", function(assert){ +QUnit.test("add_dropped_parent", function (assert) { assert.ok(edit_mode.add_dropped_parent); }); -QUnit.test("set_entity_dropable", function(assert){ +QUnit.test("set_entity_dropable", function (assert) { assert.ok(edit_mode.set_entity_dropable); }); -QUnit.test("unset_entity_dropable", function(assert){ +QUnit.test("unset_entity_dropable", function (assert) { assert.ok(edit_mode.unset_entity_dropable); }); -QUnit.test("remove_save_button", function(assert){ +QUnit.test("remove_save_button", function (assert) { assert.ok(edit_mode.remove_save_button); }); -QUnit.test("add_save_button", function(assert){ +QUnit.test("add_save_button", function (assert) { assert.ok(edit_mode.add_save_button); }); -QUnit.test("add_trash_button", function(assert){ +QUnit.test("add_trash_button", function (assert) { assert.ok(edit_mode.add_trash_button); }); -QUnit.test("add_parent_trash_button", function(assert){ +QUnit.test("add_parent_trash_button", function (assert) { assert.ok(edit_mode.add_parent_trash_button); }); -QUnit.test("add_parent_delete_buttons", function(assert){ +QUnit.test("add_parent_delete_buttons", function (assert) { assert.ok(edit_mode.add_parent_delete_buttons); }); -QUnit.test("add_property_trash_button", function(assert){ +QUnit.test("add_property_trash_button", function (assert) { assert.ok(edit_mode.add_property_trash_button); }); -QUnit.test("insert_entity", function(assert){ +QUnit.test("insert_entity", function (assert) { assert.ok(edit_mode.insert_entity); }); -QUnit.test("getProperties", function(assert){ +QUnit.test("getProperties", function (assert) { assert.ok(edit_mode.getProperties); assert.equal(edit_mode.getProperties(undefined).length, 0, "undefined returns empty list"); @@ -194,11 +201,11 @@ QUnit.test("getProperties", function(assert){ }); -QUnit.test("update_entity", function(assert){ +QUnit.test("update_entity", function (assert) { assert.ok(edit_mode.update_entity); }); -QUnit.test("add_edit_mode_button", function(assert){ +QUnit.test("add_edit_mode_button", function (assert) { assert.ok(edit_mode.add_edit_mode_button); var target = $(document.body)[0]; @@ -209,43 +216,156 @@ QUnit.test("add_edit_mode_button", function(assert){ $(button).remove(); }); -QUnit.test("toggle_edit_mode", function(assert){ +QUnit.test("toggle_edit_mode", function (assert) { assert.ok(edit_mode.toggle_edit_mode); }); -QUnit.test("leave_edit_mode", function(assert){ +QUnit.test("leave_edit_mode", function (assert) { assert.ok(edit_mode.leave_edit_mode); }); -QUnit.test("enter_edit_mode", function(assert){ +QUnit.test("enter_edit_mode", function (assert) { assert.ok(edit_mode.enter_edit_mode); }); -QUnit.test("make_header_editable", function(assert){ +QUnit.test("make_header_editable", function (assert) { assert.ok(edit_mode.make_header_editable); }); -QUnit.test("isListDatatype", function(assert){ +QUnit.test("isListDatatype", function (assert) { assert.ok(edit_mode.unListDatatype); }); -QUnit.test("make_dataype_input_logic", function(assert){ - assert.ok(edit_mode.make_dataype_input_logic); +QUnit.test("make_datatype_input_logic", function (assert) { + assert.ok(edit_mode.make_datatype_input_logic); }); -QUnit.test("make_datatype_input", function(assert){ - assert.ok(edit_mode.make_datatype_input); +QUnit.test("make_datatype_input", function (assert) { + var done = assert.async(9); + + // mock query response of server + form_elements._query = async function (q) { + log.getLogger("edit_mode").trace(q); + const entities = str2xml(`<Response> + <RecordType name="Person"/></Response>`); + return transformation.transformEntities(entities); + }; + const form_wrapper = "<form/>"; + + const no_dt_input = edit_mode.make_datatype_input(undefined); + no_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(no_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "TEXT", + "reference_scope": null, + }, "No datatype (defaults to TEXT)"); + done(); + }, true); + + const text_dt_input = edit_mode.make_datatype_input("TEXT"); + text_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(text_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "TEXT", + "reference_scope": null, + }, "TEXT"); + done(); + }, true); + + const ref_dt_input = edit_mode.make_datatype_input("REFERENCE"); + ref_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(ref_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "REFERENCE", + "reference_scope": null, + }, "REF"); + done(); + }, true); + + const file_dt_input = edit_mode.make_datatype_input("FILE"); + file_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(file_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "FILE", + "reference_scope": null, + }, "FILE"); + done(); + }, true); + + const person_dt_input = edit_mode.make_datatype_input("Person"); + person_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(person_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "REFERENCE", + "reference_scope": "Person", + }, "Person"); + done(); + }, true); + + const list_text_dt_input = edit_mode.make_datatype_input("LIST<TEXT>"); + list_text_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(list_text_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "TEXT", + "reference_scope": null, + "is_list": "on", + }, "LIST<TEXT>"); + done(); + }, true); + + const list_ref_dt_input = edit_mode.make_datatype_input("LIST<REFERENCE>"); + list_ref_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(list_ref_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "REFERENCE", + "reference_scope": null, + "is_list": "on", + }, "LIST<REFERENCE>"); + done(); + }, true); + + const list_file_dt_input = edit_mode.make_datatype_input("LIST<FILE>"); + list_file_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(list_file_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "FILE", + "reference_scope": null, + "is_list": "on", + }, "LIST<FILE>"); + done(); + }, true); + + const list_per_dt_input = edit_mode.make_datatype_input("LIST<Person>"); + list_per_dt_input.addEventListener("caosdb.field.ready", function (e) { + var obj = form_elements + .form_to_object($(form_wrapper).append(list_per_dt_input)[0]); + assert.propEqual(obj, { + "atomic_datatype": "REFERENCE", + "reference_scope": "Person", + "is_list": "on", + }, "LIST<Person>"); + done(); + }, true); + }); -QUnit.test("make_input", function(assert){ +QUnit.test("make_input", function (assert) { assert.ok(edit_mode.make_input); }); -QUnit.test("smooth_replace", function(assert){ +QUnit.test("smooth_replace", function (assert) { assert.ok(edit_mode.smooth_replace); }); -QUnit.test("make_property_editable", function(assert) { +QUnit.test("make_property_editable", function (assert) { assert.ok(edit_mode.make_property_editable); assert.throws(() => edit_mode.make_property_editable(undefined), /param 'element' is expected to be an HTMLElement, was undefined/, "undefined"); @@ -260,212 +380,214 @@ QUnit.test("make_property_editable", function(assert) { }); -QUnit.test("create_new_record", function(assert){ +QUnit.test("create_new_record", function (assert) { assert.ok(edit_mode.create_new_record); }); -QUnit.test("init_edit_app", function(assert){ +QUnit.test("init_edit_app", function (assert) { assert.ok(edit_mode.init_edit_app); }); -QUnit.test("has_errors", function(assert){ +QUnit.test("has_errors", function (assert) { assert.ok(edit_mode.has_errors); }); -QUnit.test("freeze_but", function(assert){ +QUnit.test("freeze_but", function (assert) { assert.ok(edit_mode.freeze_but); }); -QUnit.test("unfreeze", function(assert){ +QUnit.test("unfreeze", function (assert) { assert.ok(edit_mode.unfreeze); }); -QUnit.test("retrieve_datatype_list", function(assert){ +QUnit.test("retrieve_datatype_list", function (assert) { assert.ok(edit_mode.retrieve_datatype_list); }); -QUnit.test("highlight", function(assert){ +QUnit.test("highlight", function (assert) { assert.ok(edit_mode.highlight); }); -QUnit.test("unhighlight", function(assert){ +QUnit.test("unhighlight", function (assert) { assert.ok(edit_mode.unhighlight); }); -QUnit.test("handle_error", function(assert){ +QUnit.test("handle_error", function (assert) { assert.ok(edit_mode.handle_error); }); -QUnit.test("get_edit_panel", function(assert){ +QUnit.test("get_edit_panel", function (assert) { assert.ok(edit_mode.get_edit_panel); }); -QUnit.test("add_wait_datamodel_info", function(assert){ +QUnit.test("add_wait_datamodel_info", function (assert) { assert.ok(edit_mode.add_wait_datamodel_info); }); -QUnit.test("toggle_edit_panel", function(assert){ +QUnit.test("toggle_edit_panel", function (assert) { assert.ok(edit_mode.toggle_edit_panel); }); -QUnit.test("leave_edit_mode_template", function(assert){ +QUnit.test("leave_edit_mode_template", function (assert) { assert.ok(edit_mode.leave_edit_mode_template); }); -QUnit.test("is_edit_mode", function(assert){ +QUnit.test("is_edit_mode", function (assert) { assert.ok(edit_mode.is_edit_mode); }); -QUnit.test("add_cancel_button", function(assert){ +QUnit.test("add_cancel_button", function (assert) { assert.ok(edit_mode.add_cancel_button); }); -QUnit.test("create_new_entity", function(assert){ +QUnit.test("create_new_entity", function (assert) { assert.ok(edit_mode.create_new_entity); }); -QUnit.test("remove_cancel_button", function(assert){ +QUnit.test("remove_cancel_button", function (assert) { assert.ok(edit_mode.remove_cancel_button); }); -QUnit.test("freeze_entity", function(assert){ +QUnit.test("freeze_entity", function (assert) { assert.ok(edit_mode.freeze_entity); }); -QUnit.test("unfreeze_entity", function(assert){ +QUnit.test("unfreeze_entity", function (assert) { assert.ok(edit_mode.unfreeze_entity); }); -QUnit.test("filter", function(assert){ +QUnit.test("filter", function (assert) { assert.ok(edit_mode.filter); }); -QUnit.test("add_start_edit_button", function(assert){ +QUnit.test("add_start_edit_button", function (assert) { assert.ok(edit_mode.add_start_edit_button); }); -QUnit.test("remove_start_edit_button", function(assert){ +QUnit.test("remove_start_edit_button", function (assert) { assert.ok(edit_mode.remove_start_edit_button); }); -QUnit.test("add_new_record_button", function(assert){ +QUnit.test("add_new_record_button", function (assert) { assert.ok(edit_mode.add_new_record_button); }); -QUnit.test("remove_new_record_button", function(assert){ +QUnit.test("remove_new_record_button", function (assert) { assert.ok(edit_mode.remove_new_record_button); }); -QUnit.test("add_delete_button", function(assert){ +QUnit.test("add_delete_button", function (assert) { assert.ok(edit_mode.add_delete_button); }); -QUnit.test("remove_delete_button", function(assert){ +QUnit.test("remove_delete_button", function (assert) { assert.ok(edit_mode.remove_delete_button); }); { - const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } + const sleep = function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } - const datamodel = ` + const datamodel = ` <div><div class=\"btn-group-vertical\"><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 class=\"panel-heading\"><h5>Existing Properties</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('properties');\" id=\"caosdb-f-filter-properties\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input\" title=\"Create this Property.\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-20\">name</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-21\">unit</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-24\">description</li></ul></div></div><div title=\"Drag and drop RecordTypes from this panel to the Entities on the left.\" class=\"panel panel-default\"><div class=\"panel-heading\"><h5>Existing RecordTypes</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('recordtypes');\" id=\"caosdb-f-filter-recordtypes\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input\" title=\"Create this RecordType\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-30992\">Test</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-31015\">Test2</li></ul></div></div></div>`; + edit_mode.query = async function(q) { + return []; + } + QUnit.test("test case 1 - insert property", async function (assert) { - QUnit.test("test case 1 - insert property", async function(assert) { - - // here lives the test tool box - const test_tool_box = $('<div class="caosdb-f-edit-panel-body" />'); + // here lives the test tool box + const test_tool_box = $('<div class="caosdb-f-edit-panel-body" />'); - // here live the entities - const main_panel = $('<div class="caosdb-f-main-entities"/>'); - assert.equal($(".caosdb-f-main-entities").length,0); + // here live the entities + const main_panel = $('<div class="caosdb-f-main-entities"/>'); + assert.equal($(".caosdb-f-main-entities").length, 0); - $(document.body).append(test_tool_box).append(main_panel); + $(document.body).append(test_tool_box).append(main_panel); - // ENTER EDIT MODE - assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); - // fake server response - edit_mode.retrieve_data_model = async function() { - return str2xml(datamodel); - } - var app = await edit_mode.enter_edit_mode(); - assert.equal(edit_mode.is_edit_mode(), true, "now, edit_mode should be active"); + // ENTER EDIT MODE + assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); + // fake server response + edit_mode.retrieve_data_model = async function () { + return str2xml(datamodel); + } + var app = await edit_mode.enter_edit_mode(); + assert.equal(edit_mode.is_edit_mode(), true, "now, edit_mode should be active"); - // NEW PROPERTY - assert.equal($(".caosdb-f-edit-panel-new-button.new-property").length, 2, "two new-property buttons should be present"); - assert.equal($(".caosdb-entity-panel").length, 0, "no entities"); - assert.equal(app.state, "initial", "initial state"); - // click on "new property" - $(".caosdb-f-edit-panel-new-button.new-property").first().click(); + // NEW PROPERTY + assert.equal($(".caosdb-f-edit-panel-new-button.new-property").length, 2, "two new-property buttons should be present"); + assert.equal($(".caosdb-entity-panel").length, 0, "no entities"); + assert.equal(app.state, "initial", "initial state"); + // click on "new property" + $(".caosdb-f-edit-panel-new-button.new-property").first().click(); - while(app.state === "initial") { - await sleep(500); - } + while (app.state === "initial") { + await sleep(500); + } - // EDIT PROPERTY - assert.equal(app.state, "changed", "changed state"); - var entity = $(".caosdb-entity-panel"); - assert.equal(entity.length, 1, "entity added"); - // set name - $(".caosdb-entity-panel .caosdb-f-entity-name").val("TestProperty"); - - // SAVE - var save_button = $(".caosdb-f-entity-save-button"); - assert.equal(save_button.length, 1, "save button available"); - // fake server response - connection.post = async function(uri, data) { - await sleep(500); - assert.equal(xml2str(data), "<Request><Property name=\"TestProperty\" datatype=\"TEXT\"/></Request>"); - assert.equal(app.state, "wait", "in wait state"); - return str2xml("<Response><Property id=\"newId\" name=\"TestProperty\" datatype=\"TEXT\"/></Response>"); - } - // click save button - var updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); - assert.equal(updated_entity.length, 0, "entity with id not yet in main panel"); - save_button.click(); + // EDIT PROPERTY + assert.equal(app.state, "changed", "changed state"); + var entity = $(".caosdb-entity-panel"); + assert.equal(entity.length, 1, "entity added"); + // set name + $(".caosdb-entity-panel .caosdb-f-entity-name").val("TestProperty"); + + // SAVE + var save_button = $(".caosdb-f-entity-save-button"); + assert.equal(save_button.length, 1, "save button available"); + // fake server response + connection.post = async function (uri, data) { + await sleep(500); + assert.equal(xml2str(data), "<Request><Property name=\"TestProperty\" datatype=\"TEXT\"/></Request>"); + assert.equal(app.state, "wait", "in wait state"); + return str2xml("<Response><Property id=\"newId\" name=\"TestProperty\" datatype=\"TEXT\"/></Response>"); + } + // click save button + var updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); + assert.equal(updated_entity.length, 0, "entity with id not yet in main panel"); + save_button.click(); - while(app.state === "changed" || app.state === "wait" ) { - await sleep(500); - } + while (app.state === "changed" || app.state === "wait") { + await sleep(500); + } - // SEE RESPONSE - assert.equal(app.state, "initial", "initial state"); + // SEE RESPONSE + assert.equal(app.state, "initial", "initial state"); - var response = $("#newId"); - assert.equal(response.length, 1, "entity added"); + var response = $("#newId"); + assert.equal(response.length, 1, "entity added"); - // entity has been added to main panel - updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); - assert.equal(updated_entity.length, 1, "entity with new id now in main panel"); + // entity has been added to main panel + updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); + assert.equal(updated_entity.length, 1, "entity with new id now in main panel"); - //https://gitlab.com/caosdb/caosdb-webui/issues/47 - assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-entity-actions-panel").length, 1, "general actions panel there"); - assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-f-edit-mode-entity-actions-panel").length, 1, "edit_mode actions panel there (BUG caosdb-webui#47)"); + // tests for closed issue https://gitlab.com/caosdb/caosdb-webui/issues/47 + assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-entity-actions-panel").length, 1, "general actions panel there"); + assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-f-edit-mode-entity-actions-panel").length, 1, "edit_mode actions panel there (BUG caosdb-webui#47)"); - main_panel.remove(); - test_tool_box.remove(); + main_panel.remove(); + test_tool_box.remove(); - edit_mode.leave_edit_mode(); - assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); + edit_mode.leave_edit_mode(); + assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); - }); + }); } -var transformProperty = async function(xml_str) { +var transformProperty = async function (xml_str) { var xml = str2xml(`<Response>${xml_str}</Response>`); return (await transformation.transformProperty(xml)).firstElementChild; } -QUnit.test("createElementForProperty", async function(assert) { +QUnit.test("createElementForProperty", async function (assert) { assert.timeout(100000); // unique dummy values for each data type var data = { @@ -493,7 +615,7 @@ QUnit.test("createElementForProperty", async function(assert) { } }); -QUnit.test("getPropertyFromElement", async function(assert) { +QUnit.test("getPropertyFromElement", async function (assert) { assert.timeout(100000); // unique dummy values for each data type var data = { @@ -560,7 +682,7 @@ QUnit.test("getPropertyFromElement", async function(assert) { * * This test case performs a large set of different data types. */ -QUnit.test("_toggle_list_property_object", async function(assert) { +QUnit.test("_toggle_list_property_object", async function (assert) { // unique dummy values for each data type var data = { @@ -616,7 +738,9 @@ QUnit.test("_toggle_list_property_object", async function(assert) { assert.equal(list_inputs_property.listDatatype, dt, `single->list has correct listDatatype (${dt}).`); - assert.throws(() => {edit_mode._toggle_list_property_object(long_list_prop_html, false)}, /Could not toggle to list=false with value=/, "long list throws on list->single"); + assert.throws(() => { + edit_mode._toggle_list_property_object(long_list_prop_html, false) + }, /Could not toggle to list=false with value=/, "long list throws on list->single"); } }); @@ -628,7 +752,7 @@ QUnit.test("_toggle_list_property_object", async function(assert) { * _toggle_list_property_object: HTML -> JS Object -> Converted JS Object -> * Converted HTML. */ -QUnit.test("_toggle_list_property", async function(assert) { +QUnit.test("_toggle_list_property", async function (assert) { // unique dummy values for each data type var data = { @@ -670,13 +794,15 @@ QUnit.test("_toggle_list_property", async function(assert) { var list_inputs = edit_mode._toggle_list_property(non_list_prop_html, true); var single_input = edit_mode._toggle_list_property(list_prop_html, false); - assert.throws(() => {edit_mode._toggle_list_property(long_list_prop_html, false)}, /Could not toggle to list=false with value=/, "long list throws on list->single"); + assert.throws(() => { + edit_mode._toggle_list_property(long_list_prop_html, false) + }, /Could not toggle to list=false with value=/, "long list throws on list->single"); assert.equal($(list_inputs[0].parentElement).find("ol li :input:not(button)").is.length, 1, "one list input"); assert.equal($(list_inputs[0].parentElement).find("ol li :input").val(), data[dt], `list ${dt} input has correct value`); assert.equal($(single_input.parentElement).find("ol :input").length, 0, "no list input"); var val = $(single_input).val(); - if(dt=="DATETIME") { + if (dt == "DATETIME") { val = $(single_input).find("[type='date']").val() + "T" + $(single_input).find("[type='time']").val(); } assert.equal(val, data[`LIST<${dt}>`], `single ${dt} input has correct value`); @@ -699,7 +825,7 @@ QUnit.test("_toggle_list_property", async function(assert) { * (from which the user would normally choose a reference and the old reference * was pre-selected), effectively deleting the old value of the reference. */ -QUnit.test("Bug #95", async function(assert) { +QUnit.test("Bug #95", async function (assert) { var query_done; edit_mode.query = function (query) { var re = /^FIND (Record|File)\s*$/g; @@ -716,7 +842,7 @@ QUnit.test("Bug #95", async function(assert) { // old option not deleted when options are empty var resolve_function; - var empty_options = new Promise(function(res, err) { + var empty_options = new Promise(function (res, err) { resolve_function = res; }); var property = $("<div/>") @@ -725,7 +851,7 @@ QUnit.test("Bug #95", async function(assert) { datatype: "REFERENCE", list: false, value: "1234" - }, empty_options)); + }, empty_options)); assert.equal($(property).find("select").val(), "1234", "old value before"); assert.equal($(property).find("option").length, 1, "one option before"); assert.equal($(property).find(":selected").text(), "1234", "old text before"); @@ -751,7 +877,7 @@ QUnit.test("Bug #95", async function(assert) { assert.equal(options.length, 3, "3 entities returned"); var fill_method_done = assert.async(); var proxied = edit_mode.fill_reference_drop_down; - edit_mode.fill_reference_drop_down = async function(arg1, arg2) { + edit_mode.fill_reference_drop_down = async function (arg1, arg2) { await proxied(arg1, arg2); assert.equal($(property).find("select").val(), "1234", "still old value after"); @@ -766,7 +892,7 @@ QUnit.test("Bug #95", async function(assert) { datatype: "REFERENCE", list: false, value: "1234" - }, options)); + }, options)); edit_mode.fill_reference_drop_down = proxied; @@ -774,7 +900,7 @@ QUnit.test("Bug #95", async function(assert) { }); -QUnit.test("fill_reference_drop_down", async function(assert) { +QUnit.test("fill_reference_drop_down", async function (assert) { var options = edit_mode._create_reference_options(await transformation .transformEntities(str2xml(` <Response> @@ -803,9 +929,9 @@ QUnit.test("fill_reference_drop_down", async function(assert) { /** * Test the inner logic of retrieve_datatype_list. */ -QUnit.test("_create_reference_options", async function(assert) { - var entities = await transformation - .transformEntities(str2xml(` +QUnit.test("_create_reference_options", async function (assert) { + var entities = await transformation + .transformEntities(str2xml(` <Response> <Record name="RName" id="RID"/> </Response> diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js index 9b790a007435d05d1e9410774bc8c898fe287a16..c607ee28bf7888f94e3086abf8e033e1532d0d09 100644 --- a/test/core/js/modules/entity.xsl.js +++ b/test/core/js/modules/entity.xsl.js @@ -162,6 +162,9 @@ QUnit.test("LIST Property", function(assert) { }); QUnit.test("single-value template with reference property.", function(assert) { + /* DEPRECATED css class .caosdb-property-text-value - Use + * .caosdb-f-property-single-raw-value or introduce new + * .caosdb-v-property-text-value */ assert.equal(xml2str(callTemplate(this.entityXSL, 'single-value', { 'value': '', 'reference': 'true', @@ -177,6 +180,90 @@ QUnit.test("single-value template with reference property.", function(assert) { assert.equal($(link).find('.caosdb-id').length, 1, 'has caosdb-id span'); }) +QUnit.test("old version warning", function(assert) { + // with successor tag + var xmlstr = '<Record id="2345"><Version id="abcd1234"><Successor id="bcde2345"/></Version></Record>'; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find(".caosdb-entity-panel .caosdb-f-entity-version-old-warning").length, 1, "warning present"); + + // with version tag, without successor + xmlstr = '<Record id="2345"><Version id="abcd1234"/></Record>'; + xml = str2xml(xmlstr); + html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find(".caosdb-f-entity-version-old-warning").length, 0, "warning not present"); + + // without version tag + xmlstr = '<Record id="2345"></Record>'; + xml = str2xml(xmlstr); + html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find(".caosdb-f-entity-version-old-warning").length, 0, "warning not present"); +}); + +QUnit.test("version button", function(assert) { + // with version tag + var xmlstr = '<Record id="2345"><Version id="abcd1234"/></Record>'; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entities", "*"); + + assert.equal($(html).find("div.caosdb-entity-panel button.caosdb-f-entity-version-button").length, 1, "button present"); + + // without version tag + xmlstr = '<Record id="2345"></Record>'; + xml = str2xml(xmlstr); + html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find(".caosdb-f-entity-version-button").length, 0, "button not present"); +}); + +QUnit.test("version info modal", function(assert) { + // with version tag + var xmlstr = '<Record id="2345"><Version id="abcd1234"/></Record>'; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entities", "*"); + + assert.equal($(html).find("div.caosdb-entity-panel div.caosdb-f-entity-version-info").length, 1, "info present"); + + // without version tag + xmlstr = '<Record id="2345"></Record>'; + xml = str2xml(xmlstr); + html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find(".caosdb-f-entity-version-info").length, 0, "info not present"); +}); + +QUnit.test("data-version-id attribute", function(assert) { + // with version tag + var xmlstr = '<Record id="2345"><Version id="abcd1234"/></Record>'; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find("div.caosdb-entity-panel[data-version-id='abcd1234']").length, 1, "data-version-id attribute present"); + + // without version tag + xmlstr = '<Record id="2345"></Record>'; + xml = str2xml(xmlstr); + html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find("div.caosdb-entity-panel[data-version-id]").length, 0, "data-version-id attribute not present"); +}); + +QUnit.test("data-version-successor attribute", function(assert) { + // with successor tag + var xmlstr = '<Record id="2345"><Version id="abcd1234"><Successor id="bcde2345"/></Version></Record>'; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find("div.caosdb-entity-panel[data-version-successor='bcde2345']").length, 1, "data-version-successor attribute present"); + + // with version tag, without successor + xmlstr = '<Record id="2345"><Version id="abcd1234"/></Record>'; + xml = str2xml(xmlstr); + html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find("div.caosdb-entity-panel[data-version-successor]").length, 0, "data-version-successor attribute not present"); + + // without version tag + xmlstr = '<Record id="2345"></Record>'; + xml = str2xml(xmlstr); + html = applyTemplates(xml, this.entityXSL, "entities", "*"); + assert.equal($(html).find("div.caosdb-entity-panel[data-version-successor]").length, 0, "data-version-successor attribute not present"); +}); + /* MISC FUNCTIONS */ function applyTemplates(xml, xsl, mode, select = "*") { let entryRule = '<xsl:template priority="9" match="/"><xsl:apply-templates select="' + select + '" mode="' + mode + '"/></xsl:template>'; diff --git a/test/core/js/modules/ext_bottom_line.js b/test/core/js/modules/ext_bottom_line.js index 46903ead3226c1cb6038e9320548284ce76b0672..21c92167271f87554a9af881554d33db686d9194 100644 --- a/test/core/js/modules/ext_bottom_line.js +++ b/test/core/js/modules/ext_bottom_line.js @@ -45,7 +45,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { }, { "id": "test.success-2", "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) !== 'TestPreviewRecord-fall-back'", - "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}]); }" + "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}], { 'xaxis': {'title': 'time [samples]'}}); }" } ] }; diff --git a/test/core/js/modules/ext_sss_markdown.js.js b/test/core/js/modules/ext_sss_markdown.js.js new file mode 100644 index 0000000000000000000000000000000000000000..07199e869f56c7042a78dba6938de55e4a36e727 --- /dev/null +++ b/test/core/js/modules/ext_sss_markdown.js.js @@ -0,0 +1,77 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 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 + */ + + +/* MODULE ext_sss_markdown */ +QUnit.module("ext_sss_markdown", { + before: function (assert) { + markdown.init(); + ext_sss_markdown.logger.setLevel("trace"); + + const qunit_obj = this; + const done = assert.async(); + + // load test case + $.ajax({ + cache: true, + dataType: 'xml', + url: "xml/test_sss_output.xml", + }).done(function(data, textStatus, jdXHR) { + // append the test case to the QUnit module object. + qunit_obj.testCase1 = data; + }).always(function() { + done(); + }); + }, + after: function (assert) { + // clean up + $("#caosdb-stdout").remove(); + } +}); + + +QUnit.test("availability", function(assert) { + assert.ok(ext_sss_markdown, "available"); +}); + +QUnit.test("test case 1", function(assert) { + // setup + const testCase1 = this.testCase1; + const plainStdout = testCase1.evaluate("/Response/script/stdout", testCase1, null, XPathResult.STRING_TYPE, null).stringValue; + + const stdout = $('<div id="caosdb-stdout"/>').text(plainStdout); + $("body").append(stdout); + + assert.equal($("#caosdb-stdout").length, 1); + assert.equal($("#caosdb-stdout div.alert").length, 0, "no bootstrap alert"); + assert.equal($("#caosdb-stdout p").length, 0, "no html paragraphs"); + assert.equal($("#caosdb-stdout").text(), plainStdout, "only plain text"); + + ext_sss_markdown.init(); + + assert.equal($("#caosdb-stdout p").length, 3, "3 html paragraphs transformed"); + assert.equal($("#caosdb-stdout code").length, 1, "html code tag transformed"); + assert.equal($("#caosdb-stdout div.alert").text(), + "this is a bootstrap alert", "bootstrap alert transformed"); + +}); diff --git a/test/core/js/modules/ext_trigger_crawler_form.js.js b/test/core/js/modules/ext_trigger_crawler_form.js.js new file mode 100644 index 0000000000000000000000000000000000000000..e2a4b0c8a494e43234547de4740ea62dde77beac --- /dev/null +++ b/test/core/js/modules/ext_trigger_crawler_form.js.js @@ -0,0 +1,55 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 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 + */ + +'use strict'; + +QUnit.module("ext_trigger_crawler_form", { + before: () => { + $(document.body).append('<div id="top-navbar"><ul class="caosdb-navbar"/></div>'); + }, + beforeEach: () => { + $(".caosdb-f-navbar-toolbox").remove(); + }, + after: () => { + $("#top-navbar").remove(); + }, +}); + +QUnit.test("test build variables and availability", function(assert) { + assert.ok(ext_trigger_crawler_form, "availble"); + assert.equal("${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM}", "DISABLED", "BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM disabled by default."); + assert.equal("${BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX}", "Tools", "BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX defaults to Tools"); +}); + +QUnit.test("test init", function(assert) { + var tools = navbar.get_toolbox("Tools"); + + assert.equal($(tools).length, 1); + assert.equal($(tools).find("button").length, 0); + ext_trigger_crawler_form.init() + tools = navbar.get_toolbox("Tools"); + assert.equal($(tools).find("button").length, 1); + +}); + + diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 3b615e11e4bd96c26ecc0d8e1cb9d8220a4cba8e..239758161b8419a8a5e15799c67c909977f1fbf0 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -164,7 +164,6 @@ QUnit.test("get", function(assert) { }); }); - /* MODULE transformation */ QUnit.module("webcaosdb.js - transformation", { before: function(assert) { @@ -782,34 +781,29 @@ QUnit.test("getActiveSlideItemIndex", function(assert) { assert.equal(2, preview.getActiveSlideItemIndex(okElem2)); }); -QUnit.test("getEntityById", function(assert) { - assert.ok(preview.getEntityById, "function available"); +QUnit.test("getEntityByIdVersion", function(assert) { + assert.ok(preview.getEntityByIdVersion, "function available"); let e1 = $('<div><div class="caosdb-id">1</div></div>')[0]; let e2 = $('<div><div class="caosdb-id">2</div></div>')[0]; - let e3 = $('<div><div class="caosdb-id">3</div><div><div class="caosdb-id">1</div></div></div>')[0]; - let es = [e1, e2, e3]; + let es = [e1, e2]; assert.throws(() => { - preview.getEntityById() + preview.getEntityByIdVersion() }, "no param throws."); assert.throws(() => { - preview.getEntityById(null, 1) + preview.getEntityByIdVersion(null, 1) }, "null first param throws."); assert.throws(() => { - preview.getEntityById("asdf", 1) + preview.getEntityByIdVersion("asdf", 1) }, "string first param throws."); assert.throws(() => { - preview.getEntityById(es, null) + preview.getEntityByIdVersion(es, null) }, "null second param throws."); - assert.throws(() => { - preview.getEntityById(es, "asdf") - }, "string second param throws."); - assert.equal(e1, preview.getEntityById(es, 1), "find 1"); - assert.equal(e2, preview.getEntityById(es, 2), "find 2"); - assert.equal(e3, preview.getEntityById(es, 3), "find 3"); - assert.equal(null, preview.getEntityById(es, 4), "find 4 -> null"); + assert.equal(e1, preview.getEntityByIdVersion(es, "1"), "find 1"); + assert.equal(e2, preview.getEntityByIdVersion(es, "2"), "find 2"); + assert.equal(null, preview.getEntityByIdVersion(es, "3"), "find 3 -> null"); }); QUnit.test("createEmptyInner", function(assert) { @@ -870,7 +864,7 @@ QUnit.test("createCarouselNav", function(assert) { let refLinks = $('<div class="caosdb-value-list"><a><span class="caosdb-id">1234</span></a><a><span class="caosdb-id">2345</span></a><a><span class="caosdb-id">3456</span></a><a><span class="caosdb-id">4567</span></a></div>')[0]; let e1 = $('<div><div class="caosdb-id">1234</div></div>')[0]; let e2 = $('<div><div class="caosdb-id">2345</div></div>')[0]; - let e3 = $('<div><div class="caosdb-id">3456</div><div><div class="caosdb-id">1234</div></div></div>')[0]; + let e3 = $('<div><div class="caosdb-id">3456</div></div>')[0]; let e4 = $('<div><div class="caosdb-id">4567</div></div>')[0]; let entities = [e1, e3, e4, e2]; let carousel = preview.createPreviewCarousel(entities, refLinks); @@ -1040,8 +1034,41 @@ QUnit.test("preparePreviewEntity", function(assert){ assert.equal($(prepared).find('a.caosdb-id')[0].href, connection.getBasePath() + "Entity/1234", "link is correct."); }); -QUnit.test("getEntitiyIds", function(assert) { - assert.ok(preview.getEntityIds, 'function available'); +QUnit.test("getEntityRef", function(assert) { + assert.ok(preview.getEntityRef, 'function available'); + + var html = $('<div><div class="caosdb-id">sdfg</div></div>')[0]; + assert.equal(preview.getEntityRef(html), "sdfg", "id extracted"); + + html = $('<div><div class="caosdb-id"></div></div>')[0]; + assert.equal(preview.getEntityRef(html), "", "empty string extracted"); + + html = $('<div></div>')[0]; + assert.throws(()=>{preview.getEntityRef(html);}, "missing .caosdb-id throws"); +}); + + +QUnit.test("getAllEntityRefs", function(assert) { + assert.ok(preview.getAllEntityRefs, 'function available'); + assert.throws(preview.getAllEntityRefs, "null param throws"); + + // overwrite called methods + const oldGetReferenceLinks = preview.getReferenceLinks; + preview.getReferenceLinks = function(links) { + assert.propEqual(links, ["bla"], "array is passed to getReferenceLinks"); + return links; + } + const oldGetEntityRef = preview.getEntityRef; + preview.getEntityRef = function(link) { + assert.equal(link, "bla", "array elements are passed to getEntityRef"); + return "asdf"; + } + + assert.propEqual(preview.getAllEntityRefs(["bla"]), ["asdf"], "returns array with refs"); + + + // cleanup + preview.getReferenceLinks = oldGetReferenceLinks; }); QUnit.test("retrievePreviewEntities", function(assert) { @@ -1847,6 +1874,25 @@ QUnit.test("annotation module", function(assert) { /* MODULE navbar */ QUnit.module("webcaosdb.js - navbar", { + before: () => { + $(document.body).append('<div id="top-navbar"><ul class="caosdb-navbar"/></div>'); + }, + beforeEach: () => { + $(".caosdb-f-navbar-toolbox").remove(); + }, + after: () => { + $("#top-navbar").remove(); + }, +}); + +QUnit.test("get_navbar", function(assert) { + assert.equal(navbar.get_navbar().className, "caosdb-navbar"); +}); + +QUnit.test("add_button wrong parameters", function(assert) { + assert.throws(()=>{navbar.add_button(undefined)}, /button is expected/, "undefined throws"); + assert.throws(()=>{navbar.add_button({"test": "an object"})}, "object throws"); + assert.throws(()=>{navbar.add_button(["array of strings"])}, "array of string throws"); }); QUnit.test("test button classes", function(assert) { @@ -1856,3 +1902,40 @@ QUnit.test("test button classes", function(assert) { assert.ok(result.hasClass("btn-link"), "has class btn-link"); assert.equal(result.text(), "TestButton", "text is correct"); }); + +QUnit.test("add_tool", function(assert) { + assert.equal($(".caosdb-f-navbar-toolbox").length, 0, "no toolbox"); + navbar.add_tool("TestButton", "TestMenu"); + + var toolbox = $("ul.caosdb-f-navbar-toolbox"); + assert.equal(toolbox.length, 1, "new toolbox"); + assert.equal(toolbox.find("button").length, 1, "new button"); + assert.equal(toolbox.find("button").text(), "TestButton", "Name correct") + + assert.notOk(toolbox.hasClass("btn")); + assert.notOk(toolbox.hasClass("btn-link")); + assert.notOk(toolbox.hasClass("navbar-btn")); + + assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("btn")); + assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("btn-link")); + assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("navbar-btn")); +}); + +QUnit.test("toolbox example", function(assert) { + // this is a kind of integration test and it uses the toolbox_example + // module from toolbox_example.js. That example is also usefull for manual + // testing. + assert.equal($(".caosdb-f-navbar-toolbox").length, 0, "no toolbox"); + toolbox_example.init(); + assert.equal($(".caosdb-f-navbar-toolbox").length, 3, "three toolboxes"); + + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Useful Links"]').length, 1, "one 'Useful Links' toolbox"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Useful Links"] a[href="https://indiscale.com"]').length, 1, "one external link"); + + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"]').length, 1, "one 'Server-side Scripts' toolbox"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"] form input[type="submit"]').attr("value"), "Trigger Crawler", "one crawler trigger button"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"] form').attr("title"), "Trigger the crawler.", "form has tooltip"); + + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"]').length, 1, "one 'Tools' toolbox"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"] button').length, 3, "three 'Tools' buttons"); +}); diff --git a/test/core/xml/test_sss_output.xml b/test/core/xml/test_sss_output.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fad9d3edcc3436b32c0efc2a015fe5ce60a29df --- /dev/null +++ b/test/core/xml/test_sss_output.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet type="text/xsl" href="../webcaosdb.xsl"?> +<Response> + <script code="0"> + <stdout>here are new lines and stuff + +<code>this is a code environment</code> + + +<div class="alert alert-info">this is a bootstrap alert</div> + +last line ---</stdout> + </script> +</Response> diff --git a/test/ext/js/toolbox_example.js b/test/ext/js/toolbox_example.js new file mode 100644 index 0000000000000000000000000000000000000000..9b9ef6548353c248376c3c3183835d03698366da --- /dev/null +++ b/test/ext/js/toolbox_example.js @@ -0,0 +1,49 @@ +var toolbox_example = function() { + + var init = function() { + navbar.add_button("Single Button", {callback: ()=>{alert("Single Button");}, title: "Click me!"}); + navbar.add_tool("Tool 1", "Tools", {callback: ()=>{alert("Tool 1");}, title: "Tooltip 1"}); + navbar.add_tool("Tool 2", "Tools", {callback: ()=>{alert("Tool 2");}, title: "Tooltip 2"}); + navbar.add_tool("Tool 3", "Tools", {callback: ()=>{alert("Tool 3");}, title: "Tooltip 3"}); + + navbar.add_tool($('<a href="https://indiscale.com">Link1</a>')[0], "Useful Links", {title: "Browse to indiscale.com"}); + + + const script = "crawler.py" + const args = { + "-p0": "positional argument 1", + "-p1": "positional argument 2", + "-Ooption1": "option value 1", + "-Ooption2": "option value 2", + }; + const button_name = "Trigger Crawler"; + const title = "Trigger the crawler."; + + const crawler_form = make_scripting_caller_form( + script, args, button_name); + + navbar.add_tool(crawler_form, "Server-side Scripts", {title: title}); + } + + var make_scripting_caller_form = function (script, args, button_name) { + const scripting_caller = $(` + <form method="POST" action="/scripting"> + <input type="hidden" name="call" value="${script}"/> + <input type="submit" + class="btn btn-link" value="${button_name}"/> + </form>`); + + // add arguements + for (const arg in args) { + scripting_caller.append(`<input type="hidden" name="${arg}" + value="${args[arg]}"/>`); + } + + return scripting_caller[0]; + } + + return { + init: init + }; + +}();