diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0324b9326ac8e91112a3eaec46748b4dfcd3764d..07f72610a0165bb0c221671c922a0ce9232bfbe3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. variables: - CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb-webui-testenv + CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-webui/testenv # When using dind, it's wise to use the overlayfs driver for # improved performance. @@ -78,7 +78,7 @@ build-testenv: stage: setup script: - cd test/docker - - docker login -u indiscale -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY # use here general latest or specific branch latest... - docker pull $CI_REGISTRY_IMAGE:latest || true - docker build diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae4be1462bb86c43cfa3a48782cb3910a30fc29..af7186a241abbca849299410816b97e56be16de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,12 +36,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * New Dependency: loglevel-1.6.4 * Our new logging framework. Any new logging should be done with this framework. +* File Upload (v0.1 - EXPERIMENTAL) + Works only for non-LIST properties +* Edit Mode - switch the LISTishness of entity properties back and forth. ### Changed (for changes in existing functionality) * The old `caosdb-property-row` CSS class has been replaced by `caosdb-v-property-row` for styling and `caosdb-f-property` for functional needs. +* The old `caosdb-property-value` and `caosdb-property-edit-value` CSS classes + have been merged into a new `caosdb-f-property-value` class. * In [ext_xls_download.js](./src/core/js/ext_xls_download.js): Complete rewrite of the module. The generation of the TSV table is done in this module now, instead of generating it with xsl (in [query.xsl](./src/core/xsl/query.xsl)). @@ -62,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `annotation` module now (which uses the `markdown` module as back-end, tho). * updated QUnit test framework to 2.9.2 + ### Deprecated (for soon-to-be removed features) ### Removed (for now removed features) @@ -70,7 +76,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed (for any bug fixes) -* Bug in `getPropertyFromElement` (see above) which returned the unit as the property value if the actual value was an empty string. +* #95 - Edit Mode removes property values of reference properties when server + response for possible reference targets is empty. +* Bug in `getPropertyFromElement` (see above) which returned the unit as the + property value if the actual value was an empty string. ### Security (in case of vulnerabilities) diff --git a/misc/yaml_to_json.py b/misc/yaml_to_json.py index 1a7961a031923ed9280232dfec98d3a03e8c0e2a..a7d5bd62a7a1ccc50766b797ef6710466e9bee11 100755 --- a/misc/yaml_to_json.py +++ b/misc/yaml_to_json.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 import sys -import yaml import json +import yaml + with open(sys.argv[1], 'r') as infile: print(json.dumps(yaml.load(infile))) diff --git a/src/core/css/tour.css b/src/core/css/tour.css index 6fc9069fe2b8f885fc640015f10ee0eaf6c64582..3bb471369b698981f49858b8127652e957a11f8d 100644 --- a/src/core/css/tour.css +++ b/src/core/css/tour.css @@ -129,6 +129,7 @@ li.list-group-item > .btn { .popover { width: 100em; + color: initial; } /* Otherwise the pages would inherit their properties from the elements the diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js index c96881e230ef16ff929cc8fdffdf36e01a9df722..98d4feae395f47a0eaecb420ed99f231f4fbf70b 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -436,7 +436,7 @@ function getPropertyName(element) { function getPropertyFromElement(propertyelement, names = undefined) { let property = {}; - let valel = propertyelement.getElementsByClassName("caosdb-property-value")[0]; + let valel = propertyelement.getElementsByClassName("caosdb-f-property-value")[0]; let dtel = propertyelement.getElementsByClassName("caosdb-property-datatype")[0]; let idel = propertyelement.getElementsByClassName("caosdb-property-id")[0]; let unitel = valel.getElementsByClassName("caosdb-unit")[0]; @@ -482,8 +482,8 @@ function getPropertyFromElement(propertyelement, names = undefined) { } - if (!property.list && valel.getElementsByClassName("caosdb-property-text-value").length == 1) { - valel = valel.getElementsByClassName("caosdb-property-text-value")[0]; + if (!property.list && valel.getElementsByClassName("caosdb-f-property-single-raw-value").length == 1) { + valel = valel.getElementsByClassName("caosdb-f-property-single-raw-value")[0]; } var value_string = undefined; @@ -506,7 +506,10 @@ function getPropertyFromElement(propertyelement, names = undefined) { if (typeof value_string !== "undefined") { // This is set to true, when there is a reference or a list of references: - property.reference = (valel.getElementsByClassName("caosdb-id").length > 0); + if(typeof property.reference === "undefined") { + property.reference = (valel.getElementsByClassName("caosdb-id").length > 0); + } + if (property.list) { // list datatypes let listel; @@ -525,7 +528,7 @@ function getPropertyFromElement(propertyelement, names = undefined) { property.value.push(listel[j].textContent); } } - } else if (property.reference) { + } else if (property.reference && valel.getElementsByTagName("a")[0]) { // reference datatypes property.value = getIDfromHREF(valel.getElementsByTagName("a")[0]); } else { @@ -653,7 +656,7 @@ function setProperty(element, property) { dindex--; continue; } - setPropertySafe(res[i].getElementsByClassName("caosdb-property-value")[0], + setPropertySafe(res[i].getElementsByClassName("caosdb-f-property-value")[0], property, propold); counter++; diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index 692ae79ab9847b62ef06a3349e2ac9bf2bebda93..96ff91d2d9a2a6e153b56e8f6b66b6669f93a70b 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -29,6 +29,8 @@ */ var edit_mode = new function() { + var logger = log.getLogger("edit_mode"); + /** * Fired by the new property element *after* it has been added to the * entity. @@ -40,6 +42,22 @@ var edit_mode = new function() { */ this.start_edit = new Event("caosdb.edit_mode.start_edit") + /** + * Fired by a list-property when an (input) element is added to the list during editing. + */ + this.list_value_input_added = new Event("caosdb.edit_mode.list_value_input_added"); + + /** + * Fired by a list-property when an (input) element is removed to the list during editing. + */ + this.list_value_input_removed = new Event("caosdb.edit_mode.list_value_input_removed"); + + + /** + * Fired by a property when the data type changes. + */ + this.property_data_type_changed = new Event("caosdb.edit_mode.property_data_type_changed"); + this.init = function() { if (isAuthenticated()) { var target = $("#top-navbar").find("ul").first(); @@ -115,7 +133,7 @@ var edit_mode = new function() { var prop_id = tmp_id[tmp_id.length - 1]; var entity_type = tmp_id[tmp_id.length - 2]; if (entity_type == "p") { - console.log("SHOULD not happend") + logger.warn("SHOULD not happend") } else if (entity_type == "rt") { var name = $("#" + propsrcid).text(); var dragged_rt = str2xml('<Response><RecordType id="' + prop_id + '" name="' + name + '"></RecordType></Response>'); @@ -157,7 +175,7 @@ var edit_mode = new function() { // Dropping a RecordType in the heading will add it as a parent. // This is done by this function. this.parent_drop = function(e) { - console.assert(edit_mode.app.state === "changed", "state should be changed. Current state: ", edit_mode.app.state, edit_mode.app, e); + logger.assert(edit_mode.app.state === "changed", "state should be changed. Current state: ", edit_mode.app.state, edit_mode.app, e); e.preventDefault(); edit_mode.unhighlight(); edit_mode.add_dropped_parent(e, this); @@ -207,9 +225,11 @@ var edit_mode = new function() { $(save_btn).click(callback); } - /* TODO + /* + * This is unused functionality which might be added as an expert option later on. + * this.add_edit_xml_button = function(entity) { - var button = $('<button class="btn btn-link caosdb-update-entity-button" title="Edit the XML representaion of this entity."><span class="glyphicon glyphicon-pencil"/> Edit XML</button>'); + var button = $('<button class="btn btn-link caosdb-update-entity-button" title="Edit the XML representation of this entity."><span class="glyphicon glyphicon-pencil"/> Edit XML</button>'); $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").append(button); var callback = (e) => { @@ -257,15 +277,26 @@ var edit_mode = new function() { * bind a remove on the deletable. */ this.add_property_trash_button = function(appendable, deletable) { - edit_mode.add_trash_button(appendable, deletable, "caosdb-f-property-trash-button"); + edit_mode.add_trash_button(appendable, deletable, "caosdb-f-property-trash-button", undefined, "Remove this property"); } /** * Append a trash button which removes the deletable and calls an optional - * callback function + * callback function. + * + * @param {HTMLElement} appendable - add the button to this element. + * @param {HTMLElement} deletable - delete this element on click. + * @param {string} className - a class name for the button element. + * @param {function} [callback] - optional parameterless callback function + * which is called on click. + * @param {string} [title] - optional title for the button element. + * @return {undefined} */ - this.add_trash_button = function(appendable, deletable, className, callback = undefined) { + this.add_trash_button = function(appendable, deletable, className, callback = undefined, title = undefined) { var button = $('<button class="btn btn-link ' + className + ' caosdb-f-entity-trash-button"><span class="glyphicon glyphicon-trash"></span></button>'); + if(title) { + button.attr("title", title); + } $(appendable).append(button); button.click((e) => { e.stopPropagation(); @@ -308,17 +339,57 @@ var edit_mode = new function() { return await insert(xmls); } + /** - * Check whether datatypestring equals one of the datatypes in - * the array datatypelist. - */ - this.checkForDatatypeList = function (datatypestring, datatypelist) { - for (var i=0; i<datatypelist.length; i++) { - if (datatypestring == datatypelist[i]) { - return true; + * TODO merge with getPropertyFromElement in caosdb.js + */ + this.getPropertyFromElement = function(element) { + var editfield = $(element).find(".caosdb-f-property-value"); + var property = getPropertyFromElement(element); + + var _parse_single_datetime = function(field) { + let time = $(field).find(":input[type='time']").val() + let date = $(field).find(":input[type='date']").val(); + if (time) { + return input2caosdbDate(date, time); + } else { + return date; } } - return false; + + // LISTs need to be handled here + if (property.list == true) { + property.value = []; + if (["TEXT","DOUBLE","INTEGER"].includes(property.listDatatype)) { + // LOOP over elements of editfield.find(":input") + for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input:not(.caosdb-unit)")) { + property.value.push($(singleelement).val()); + } + } else if (property.listDatatype == "DATETIME") { + for (let singleelement of $(editfield).find("li")) { + property.value.push(_parse_single_datetime(singleelement)); + } + } else if (property.reference || property.listDatatype == "BOOLEAN") { + // LOOP over elements of editfield.find("select") + for (var singleelement of $(editfield).find("select")) { + property.value.push($(singleelement).val()); + } + } else { + throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); + } + } else { + if (["TEXT","DOUBLE","INTEGER"].includes(property.datatype)) { + property.value = editfield.find(":input:not(.caosdb-unit)").val(); + } else if (property.datatype == "DATETIME") { + property.value = _parse_single_datetime(editfield); + } else if (property.reference || property.datatype == "BOOLEAN") { + property.value = $(editfield).find("select").val(); + } else { + throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); + } + } + property.unit = editfield.find(".caosdb-unit").val(); + return property; } /** @@ -335,50 +406,7 @@ var edit_mode = new function() { const prop_elements = getPropertyElements(ent_element); for (var element of prop_elements) { - var valfield = $(element).find(".caosdb-property-value"); - var editfield = $(element).find(".caosdb-property-edit-value"); - var property = getPropertyFromElement(element); - - // LISTs need to be handled here - if (property.list == true) { - // TODO: unit missing - property.value = []; - if (this.checkForDatatypeList(property.listDatatype, - ["TEXT","DATE","DOUBLE","INTEGER","BOOLEAN","FILE"])) { - // LOOP over elements of editfield.find(":input") - for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input")) { - property.value.push($(singleelement).val()); - } - } else if (property.datatype == "DATETIME") { - throw ("Lists of DATETIME currently not supported."); - } else if (property.reference) { - // LOOP over elements of editfield.find("select") - for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input")) { - property.value.push(singleelement.selectedOptions[0].value); - } - } else { - throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); - } - } else { - property.unit = editfield.find(".caosdb-unit").val(); - if (this.checkForDatatypeList(property.datatype, - ["TEXT","DATE","DOUBLE","INTEGER","BOOLEAN","FILE"])) { - property.value = editfield.find(":input").val() - } else if (property.datatype == "DATETIME") { - let es = editfield.find(":input"); - if (es.length == 2) { - property.value = input2caosdbDate( - es[0].value, - es[1].value); - } else if (es[0]) { - property.value = es[0].value; - } - } else if (property.reference) { - property.value = $(editfield).find("select").first()[0].selectedOptions[0].value; - } else { - throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); - } - } + var property = edit_mode.getPropertyFromElement(element); properties.push(property); } } @@ -514,7 +542,9 @@ var edit_mode = new function() { this.make_input("description", getEntityDescription(entity)), ]; if (getEntityRole(roleElem[0]) == "Property") { - for (const input of this.make_datatype_input(getEntityDatatype(entity), getEntityUnit(entity))) { + // 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(); @@ -592,13 +622,19 @@ var edit_mode = new function() { } /** - * Helper function that creates an HTML string for a specific property. + * Create an input element for a single property's value. + * + * @param {object} property - entity property object. + * @param {HTMLElements[]} [options] - an array of OPTION elements + * which represent possible candidates for reference values. The + * options will be appended to the SELECT input. This parameter is + * optional and only used for reference properties. This parameter + * might as well be a Promise for such an array. */ - this.createElementstringForProperty = function(property) { - var editelementstring; + this.createElementForProperty = function(property, options) { + var result; if (property.datatype == "TEXT") { - //editelementstring = "<input type='text' value='" + property.value + "'></input>"; - editelementstring = "<textarea>" + property.value + "</textarea>"; + result = "<textarea>" + ( property.value || "" ) + "</textarea>"; } else if (property.datatype == "DATETIME") { var dateandtime = [""]; if(property.value) { @@ -607,40 +643,52 @@ var edit_mode = new function() { let date = dateandtime[0]; if (dateandtime.length == 2) { let time = dateandtime[1]; - editelementstring = "<input type='date' value='" + date + "'></input>" + - "<input type='time' value='" + time + "'></input>"; + result = "<span><input type='date' value='" + date + "'/>" + + "<input type='time' value='" + time + "'/></span>"; } else { - editelementstring = "<input type='date' value='" + date + "'></input>"; + result = "<input type='date' value='" + date + "'/>"; } } else if (property.datatype == "DOUBLE") { - editelementstring = "<input type='number' step='any' value='" + property.value + "'></input><input class='caosdb-unit' title='unit' style='width: 60px;' placeholder='unit' value='" + (typeof property.unit == 'undefined' ? "" : property.unit) + "' type='text'></input>"; + result = "<input type='number' step='any' value='" + property.value + "'></input>"; } else if (property.datatype == "INTEGER") { - editelementstring = "<input type='number' value='" + property.value + "'></input><input class='caosdb-unit' title='unit' style='width: 60px;' placeholder='unit' value='" + (typeof property.unit == 'undefined' ? "" : property.unit) + "' type='text'></input>"; - } else if (property.datatype == "FILE") { - editelementstring = "<input type='text' value='" + property.value + "'></input>"; + result = "<input type='number' value='" + property.value + "'></input>"; } else if (property.datatype == "BOOLEAN") { - editelementstring = $('<select class="form-control caosdb-list-' + property.datatype + '"><option selected value=""></option><option value="FALSE">FALSE</option><option value="TRUE">TRUE</option></select>'); - $(editelementstring).val(property.value); - } else if (property.reference) { - editelementstring = '<select style="width:80%;display:inline;" class="form-control caosdb-list-' + property.datatype + '" data-resolved="false"><option selected class="caosdb-f-option-default" value="' + property.value + '">' + property.value + '</option><option></option></select>'; + result = $('<select style="width:80%;display:inline;" class="form-control caosdb-list-' + property.datatype + '"><option value=""></option><option value="FALSE">FALSE</option><option value="TRUE">TRUE</option></select>'); + result.val(property.value); + } else if (property.reference || property.datatype == "FILE") { + result = $('<select style="width:80%;display:inline;" class="form-control caosdb-list-' + property.datatype + '" data-resolved="false"><option selected class="caosdb-f-option-default" value="' + property.value + '">' + property.value + '</option></select>'); + if (typeof options !== "undefined") { + edit_mode.fill_reference_drop_down(result[0], options); + } } else { throw ("Unsupported data type: `" + property.datatype + "`. Please issue a feature request."); } - return editelementstring; + return $(result)[0]; } - this.generate_list_item_control_panel = function(property) { + /** + * Return a span which contains plus button and trash button. + * + * This panel is used to insert and remove elements from list properties. + * + * @param {object} property - a property object. + * @returns {HTMLElement} a SPAN element. + */ + this.generate_list_item_control_panel = function(property, options) { // Add list delete buttons: var deleteButton = $('<button title="Delete this list element." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><span class="glyphicon glyphicon-trash"></span></button>'); $(deleteButton).click(function() { + var ol = this.parentElement.parentElement.parentElement; + $(this.parentElement.parentElement).remove(); + ol.dispatchEvent(edit_mode.list_value_input_removed); }); - - + + // Add list insert buttons: var insertButton = $('<button title="Insert a new list element before this element." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><span class="glyphicon glyphicon-plus"></span></button>'); $(insertButton).click(function() { - var newelementstring = ""; + // TODO MERGE WITH OTHER PLACES WHERE THIS STRING APPEARS var proptemp = { list: false, reference: property.reference, @@ -650,82 +698,280 @@ var edit_mode = new function() { id: undefined, datatype: property.listDatatype }; - newelementstring += "<li>" + edit_mode.createElementstringForProperty(proptemp) + "</li>"; - $(newelementstring).append( - edit_mode.generate_list_item_control_panel(property)).insertBefore($(this.parentElement.parentElement)); + $("<li/>") + .append(edit_mode.createElementForProperty(proptemp, options)) + .append(edit_mode.generate_list_item_control_panel(property, options)) + .insertBefore(this.parentElement.parentElement); - edit_mode.retrieve_datatype_list(property.listDatatype); + // dispatch on <ol> element + this.parentElement.parentElement.parentElement.dispatchEvent(edit_mode.list_value_input_added); }); - return $("<span></span>").append(deleteButton).append(insertButton); + return $("<span></span>").append(deleteButton).append(insertButton)[0]; } + /** - * @param {HTMLElement} entity property in view mode representation. - * @return {undefined} + * */ - this.make_property_editable = function(element) { - if (typeof element == "undefined") { - throw Error("parameter `element` was undefined."); - } + this.create_unit_field = function(unit) { + return $(`<input class='caosdb-unit' title='unit' style='width: 60px;' placeholder='unit' value='${unit||""}' type='text'></input>`)[0]; + } - edit_mode.add_property_trash_button($(element).find(".caosdb-property-edit")[0], element); - var valfield = $(element).find(".caosdb-property-value"); - var editfield = $(element).find(".caosdb-property-edit-value"); - var property = getPropertyFromElement(element); - valfield.hide(); - editfield.show(); - editfield.text(""); - var editelementstring; - if (property.list == false) { - editelementstring = edit_mode.createElementstringForProperty(property); - if (property.reference == true) { - edit_mode.retrieve_datatype_list(property.datatype); + /** + * Create input elements for a property's value. + * + * The created input element's type depends on the data type of the + * property, e.g. 'textarea' for TEXT properties, 'select' for LISTs and so + * on. + * + * @param {object} property - a property object. + * @return {HTMLElement[]} + */ + this.create_value_inputs = function(property) { + logger.trace("enter create_value_inputs", arguments); + + var result = [ ]; + if (!property.list) { + var options = property.reference ? edit_mode.retrieve_datatype_list(property.datatype) : undefined; + result.push(edit_mode.createElementForProperty(property, options)); + if (["DOUBLE", "INTEGER"].includes(property.datatype)) { + result.push(edit_mode.create_unit_field(property.unit)); } } else { - editelementstring = "<ul>"; + var options = property.reference ? edit_mode.retrieve_datatype_list(property.listDatatype) : undefined; + var inputs = $('<ol style="list-style-type: none; padding: 0; margin:0;"/>'); + + // unit_field + if (["DOUBLE", "INTEGER"].includes(property.listDatatype)) { + result.push(edit_mode.create_unit_field(property.unit)); + } + for (var i=0; i<property.value.length; i++) { + // TODO MERGE WITH OTHER PLACES WHERE THIS STRING APPEARS var proptemp = { list: false, reference: property.reference, value: property.value[i], - unit: property.unit, name: undefined, id: undefined, datatype: property.listDatatype }; - editelementstring += "<li>" + edit_mode.createElementstringForProperty(proptemp) + "</li>"; - edit_mode.retrieve_datatype_list(property.listDatatype); + var new_element = $("<li/>") + .append(edit_mode.createElementForProperty(proptemp, options)) + .append(edit_mode.generate_list_item_control_panel(property, options)); + + inputs.append(new_element); } - editelementstring += "</ul>"; + + // PLUS-button for appending inputs to the list. + var insertButton = $('<button title="Append a new field at the end." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><span class="glyphicon glyphicon-plus"></span></button>'); + $(insertButton).click(function() { + // TODO MERGE WITH OTHER PLACES WHERE THIS STRING APPEARS + var proptemp = { + list: false, + reference: property.reference, + value: "", + name: undefined, + id: undefined, + datatype: property.listDatatype + }; + + var new_element = $("<li/>") + .append(edit_mode.createElementForProperty(proptemp, options)) + .append(edit_mode.generate_list_item_control_panel(property, options)); + + inputs.append(new_element); + inputs[0].dispatchEvent(edit_mode.list_value_input_added); + + }); + + result = result.concat([inputs[0], + $("<span>Insert element at the end of the list: </span>") + .append(insertButton)[0]]); } - editfield.append($(editelementstring)); + return result; - $(editfield).find("li").append(edit_mode.generate_list_item_control_panel(property)); + } - // Add a single list insert button at the end of each list: - var insertButton = $('<button title="Insert a new list element before this element." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><span class="glyphicon glyphicon-plus"></span></button>'); - $(insertButton).click(function() { - var newelementstring = ""; - var proptemp = { - list: false, - reference: property.reference, - value: "", - unit: property.unit, - name: undefined, - id: undefined, - datatype: property.listDatatype - }; - newelementstring += "<li>" + edit_mode.createElementstringForProperty(proptemp) + "</li>"; - $($(this.parentElement.parentElement).find("ul")).append($(newelementstring).append( - edit_mode.generate_list_item_control_panel(property))); - edit_mode.retrieve_datatype_list(property.listDatatype); + /** + * Return a property object for the property element where the LISTishness + * is toggled. + * + * This function returns `undefined` if the property already has the + * desired state. + * + * @param {HTMLElement} element - entity property in HTML representation. + * @param {boolean} toList - switch the LISTishness of the property on/off. + * @returns {object} property object when the switch was successful, + * or `undefined` if the property was already in the desired state. + * + * @throws {Error} if the LIST property has more than one element and is + * about to be converted to a non-LIST property (which is not allowed, + * because only the other elements would not fit into a non-LIST + * property). + * + * @see {@link _toggle_list_property} which is the most prominent callee. + */ + this._toggle_list_property_object = function(element, toList) { + const property = edit_mode.getPropertyFromElement(element); + + // property.list XAND toList + if(property.list ? toList : !toList) { + // already in desired state. + return undefined; + } + property.list = toList; + let datatype = toList ? `LIST<${property.datatype}>` : property.listDatatype || property.datatype; + property.listDatatype = toList ? property.datatype : undefined; + property.datatype = datatype; + + // transform value + if (!toList && $.isArray(property.value) && property.value.length < 2) { + property.value = property.value[0] || ""; + } else if (toList && !$.isArray(property.value)) { + property.value = [ property.value ] + } else { + throw new Error(`Could not toggle to list=${toList} with value=${property.value}.`); + } + + return property; + } + + /** + * Change the data type and the input fields of an entity property from the + * LIST version to the atomic version of the same data type back and forth. + * + * @param {HTMLElement} element - entity property in HTML representation. + * @param {boolean} toList - toggle list on or off. + * @returns {HTMLElement|HTMLElement[]} the generated inputs (for testing + * purposes). Returns `undefined` when no changes. + * + * @see {@link _toggle_list_property_object} which does the actual + * conversion work. + */ + this._toggle_list_property = function(element, toList) { + logger.trace("enter _toggle_list_property", arguments); + + let property = edit_mode._toggle_list_property_object(element, toList); + + if(!property) { + // already in desired state. + return; + } + + let inputs = edit_mode + .create_value_inputs(property); + + // remove old content if present + var editfield = $(element).find(".caosdb-f-property-value"); + editfield.children().remove(); + editfield.append(inputs); + + edit_mode.change_property_data_type(element, property.datatype); + + return inputs; + } + + + this.change_property_data_type = function(element, datatype) { + $(element).find(".caosdb-property-datatype").text(datatype); + element.dispatchEvent(edit_mode.property_data_type_changed); + } + + /** + * Add a checkbox 'LIST [ ]' which switches between list and non-list datatype. + * + * @param {HTMLElement} element - entity property in HTML representation. + * @param {boolean} list - whether the property is a list, initially. + * @param {string} datatype - the initial data type of the property. + */ + this.add_toggle_list_checkbox = function (element, list, datatype) { + var editfield = $(element).find(".caosdb-f-property-value"); + var label = "List "; + var checkbox = $('<input type="checkbox" class="caosdb-f-entity-is-list"/>'); + $(element).find(".caosdb-property-edit").prepend(checkbox).prepend(label); + + checkbox.prop("checked", list); + + // bind _toggle_list_property to checkbox + checkbox.change(() => { + try { + edit_mode._toggle_list_property(element, $(checkbox).prop("checked")); + } catch (err) { + globalError(err); + } }); - ($("<span>Insert element at the end of the list: </span>").append($(insertButton))).insertAfter($(editfield).find("ul")); + // disable checkbox when list has more than 1 element + let disabled = editfield.find("li").length > 1; + checkbox.prop("disabled", disabled); + const listDatatype = list ? datatype.substring(5, datatype.length-1) : datatype; + if (disabled) { + checkbox.attr("title", `You must remove all elements of the list but one by clicking the trash buttons (to the left) before you can toggle the LIST<${listDatatype}>`); + } else { + checkbox.attr("title", `Toggle LIST<${listDatatype}> data type of this property.`); + } + // disable the checkbox when elements are added to the list and the + // list has more than one element. + editfield[0].addEventListener( + edit_mode.list_value_input_added.type, (e) => { + let disabled = editfield.find("li").length > 1; + checkbox.prop("disabled", disabled); + if (disabled) { + checkbox.attr("title", `You must remove all elements of the list but one by clicking the trash buttons (to the left) before you can toggle the LIST<${listDatatype}>`); + } + }, true); + + // enable the checkbox when elements removed to the list and the number + // of elements is smaller than 2. + editfield[0].addEventListener( + edit_mode.list_value_input_removed.type, (e) => { + let disabled = editfield.find("li").length > 1; + checkbox.prop("disabled", disabled); + if (!disabled) { + checkbox.attr("title", `Toggle LIST<${listDatatype}> data type of this property.`); + } + }, true); + } + + /** + * Hide the read-only property value and create an input element for the + * property value. + * + * The new input element for the property value reflects the special needs + * of each data type, e.g. drop-down menues for REFERENCE properties, an + * ordered list with buttons for inserting and deleting elements for + * LIST<?> properties, text areas for TEXT properties and so on. + * + * Also, a checkbox `LIST[ ]`, and a trash button are added. The former is + * for toggling the data type of the property back and forth from + * LIST<Person> to Person or similar. The trash button removes the property + * from the entity. + * + * @param {HTMLElement} element - entity property in HTML representation. + */ + this.make_property_editable = function(element) { + caosdb_utils.assert_html_element(element, "param 'element'"); + + var editfield = $(element).find(".caosdb-f-property-value"); + var property = getPropertyFromElement(element); + + + // create inputs + var inputs = edit_mode + .create_value_inputs(property); + editfield.children().remove(); + editfield.append(inputs); + + // CHECKBOX `List [ ]` + edit_mode.add_toggle_list_checkbox(element, property.list, property.datatype); + + // TRASH BUTTON + edit_mode.add_property_trash_button($(element).find(".caosdb-property-edit")[0],element); } this.create_new_record = async function(recordtype_id, name = undefined) { @@ -944,11 +1190,7 @@ var edit_mode = new function() { edit_mode.handle_error(e); } }; - app.onBeforeTransition = function(e) { - console.log(e); - } app.onAfterTransition = function(e) { - console.log(e); init_drag_n_drop(); } app.onEnterInitial = async function(e) { @@ -1129,19 +1371,11 @@ var edit_mode = new function() { } - // TODO: write generic function format property depending on datatype and the property - - this.retrieve_datatype_list = async function(datatype) { - var entities = await edit_mode.query("FIND Record " + datatype); - var files = await edit_mode.query("FIND File " + datatype); + this._create_reference_options = function(entities) { + var results = []; - for (var i = 0; i < entities.length + files.length; i++) { - - if (i < entities.length) { - var eli = entities[i]; - } else { - var eli = files[i]; - } + for (var i = 0; i < entities.length; i++) { + var eli = entities[i]; var prlist = getProperties(eli); var prdict = []; // The name is not included in the property list. @@ -1153,22 +1387,65 @@ var edit_mode = new function() { prdict.push(prlist[j].name + ": " + prlist[j].value); } prdict.push("CaosDB ID: " + getEntityID(eli)); - $("select.caosdb-list-" + datatype).not('[data-resolved="true"]').append( - $("<option value=\"" + getEntityID(eli) + "\">" + prdict.join(", ") + "</option>")); + results.push($(`<option value="${getEntityID(eli)}"/>`).text(prdict.join(", "))[0]); } - - var elist = $("select.caosdb-list-" + datatype).not('[data-resolved="true"]'); - for (var i=0; i<elist.length; i++) { - var defauopt = $(elist[i]).find("option.caosdb-f-option-default")[0]; - var val = defauopt.value; - console.log(defauopt); - $(defauopt).remove(); - console.log(val); - console.log(elist[i]); - $(elist[i]).find("[value='" + val + "']").attr("selected", "selected"); + + return results; + } + + /** + * 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). + * + * @param {HTMLElement} drop_down - A SELECT element which will be filled. + * @param {HTMLElement[]} options - array of option elements, ready for + * being appended to a SELECT element. This parameter might as well be + * a Promise for such an array. + */ + this.fill_reference_drop_down = async function (drop_down, options) { + var resolved_options = await options; + var old = $(drop_down).find("option.caosdb-f-option-default"); + if(old.length == 0) { + // no option selected, append all + $(drop_down).append($(resolved_options).clone()); + return; + } + var old_value = old.attr("value"); + for (let opt of resolved_options) { + if (opt.value === old_value) { + old.text($(opt).text()); + } else { + $(drop_down).append($(opt).clone()); + } } - - $("select.caosdb-list-" + datatype).not('[data-resolved="true"]').attr("data-resolved", "true"); + } + + /** + * Retrieve all candidates for reference values depending on the datatype + * of a property and populate the value's drop down menu with options. + * + * E.g retrieve all "Person" records for a property with data type `Person` + * or `LIST<Person>`. + * + * @async + * @param {string} datatype - the data type of the reference property. + * @returns {HTMLElement[]} array of OPTION element, representing an entity + * which can be referenced by the property. + */ + this.retrieve_datatype_list = async function(datatype) { + var find_entity = ["FILE", "REFERENCE"].includes(datatype) ? "" : datatype; + var entities = datatype !== "FILE" ? await edit_mode.query(`FIND Record ${find_entity}`) : []; + var files = await edit_mode.query(`FIND File ${find_entity}`); + + var options = edit_mode + ._create_reference_options(entities) + .concat(edit_mode + ._create_reference_options(files)); + + return options; + } this.highlight = function(entity) { diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index b62fcaac7a7b503fa8d96ef4fb046887efd111b1..9e11ac70642fee2b1f3371bebb21c68f0ab949da 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -586,7 +586,7 @@ var resolve_references = new function () { this.get_resolvable_properties = function (container) { const _magic_class_name = this._unresolved_class_name; - return $(container).find(".caosdb-property-value").has( + return $(container).find(".caosdb-f-property-value").has( `.${_magic_class_name}`).toArray(); } diff --git a/src/core/js/ext_xls_download.js b/src/core/js/ext_xls_download.js index d8953840c580fa742ef35f6fdd3981748c1e0d35..0f607a459b8e502e1d4c451b1636d99ceb01657c 100644 --- a/src/core/js/ext_xls_download.js +++ b/src/core/js/ext_xls_download.js @@ -137,7 +137,7 @@ var caosdb_table_export = new function () { */ this._get_property_value = function(property) { const value_element = $(property) - .find(".caosdb-property-value") + .find(".caosdb-f-property-value") .first(); const raw_value = value_element .find(".caosdb-f-property-single-raw-value") diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index 28226e80abe4bd392563e03d2d301c3589b669d8..5c29058f532dc30daa98b6354b60e3caab88a7a6 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -1135,13 +1135,13 @@ var form_elements = new function () { let label = this._make_input_label_str(config); let type = config.type; let value = config.value; - let input = $('<input class="form-control caosdb-property-text-value" type="' + type + + let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + '" name="' + name + '" />'); input.change(function () { ret[0].dispatchEvent(form_elements.field_changed_event); }); - let input_col = $('<div class="caosdb-property-value col-sm-9"/>'); + let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>'); input_col.append(input); if (value) { input.val(value); diff --git a/src/core/js/preview.js b/src/core/js/preview.js index 4255b86d4f24a6eae3aeae13a61b3a54bbec8245..04a7b699b2d1adae5143f7362e965828c3d61531 100644 --- a/src/core/js/preview.js +++ b/src/core/js/preview.js @@ -187,7 +187,7 @@ var preview = new function() { * @return {HTMLElement} The parameter `property`. */ this.addPreview = function(property, previewContainer) { - property.getElementsByClassName("caosdb-property-value")[0].appendChild(previewContainer); + property.getElementsByClassName("caosdb-f-property-value")[0].appendChild(previewContainer); property.dispatchEvent(preview.previewReadyEvent); return property; } @@ -220,7 +220,7 @@ var preview = new function() { * @return {HTMLElement} The parameter `property`. */ this.addNotificationArea = function(property, notificationArea) { - property.getElementsByClassName("caosdb-property-value")[0].appendChild(notificationArea); + property.getElementsByClassName("caosdb-f-property-value")[0].appendChild(notificationArea); return property; } @@ -251,28 +251,28 @@ var preview = new function() { /** * Add a showPreviewButton to a reference property's value section. * - * The button is appended to the first occuring element with class `caosdb-property-value`. + * The button is appended to the first occuring element with class `caosdb-f-property-value`. * * @param {HTMLElement} ref_property_elem * @param {HTMLElement} buttom_elem * @return {HTMLElement} parameter `ref_property_elem` */ this.addShowPreviewButton = function(ref_property_elem, button_elem) { - ref_property_elem.getElementsByClassName("caosdb-property-value")[0].appendChild(button_elem); + ref_property_elem.getElementsByClassName("caosdb-f-property-value")[0].appendChild(button_elem); return ref_property_elem; } /** * Add a hidePreviewButton to a reference property's value section. * - * The button is appended to the first occuring element with class `caosdb-property-value`. + * The button is appended to the first occuring element with class `caosdb-f-property-value`. * * @param {HTMLElement} ref_property_elem * @param {HTMLElement} buttom_elem * @return {HTMLElement} The parameter `ref_property_elem`. */ this.addHidePreviewButton = function(ref_property_elem, button_elem) { - ref_property_elem.getElementsByClassName("caosdb-property-value")[0].appendChild(button_elem); + ref_property_elem.getElementsByClassName("caosdb-f-property-value")[0].appendChild(button_elem); return ref_property_elem; } @@ -320,7 +320,7 @@ var preview = new function() { } let refLinksList = $(ref_property_elem).find('.caosdb-value-list').has('.caosdb-id')[0]; if (refLinksList == null) { - return $(ref_property_elem).find('.caosdb-property-value > .btn').has('.caosdb-id')[0]; + return $(ref_property_elem).find('.caosdb-f-property-value > .btn').has('.caosdb-id')[0]; } return refLinksList } diff --git a/src/core/js/tour.js b/src/core/js/tour.js index d798eb558266179483c7be7ebf37f5f083e78a1c..57b426818abf2da0d58b46db3ce5bd893721bcc4 100644 --- a/src/core/js/tour.js +++ b/src/core/js/tour.js @@ -481,8 +481,14 @@ var tour = new function() { $(button).css("right", - Math.abs($(button).outerWidth()) / 2); break; case "bottom": + wrapper.css("top", "100%"); + wrapper.css("left", "50%"); + wrapper.css("position", "absolute"); sel.append(wrapper); - $(button).css("bottom", - Math.abs($(button).outerHeight()) / 2); + $(button).css("margin-top", "5px"); + $(button).css("top", 0); + $(button).css("left", 0); + $(button).css("transform", "translate(-50%, 0)"); break; case "bottom-left": sel.append(wrapper); @@ -493,8 +499,10 @@ var tour = new function() { wrapper.css("top", "50%"); wrapper.css("position", "absolute"); sel.prepend(wrapper); - $(button).css("top", - Math.abs($(button).outerHeight()) / 2); - $(button).css("left", - Math.abs($(button).outerWidth()) / 2); + $(button).css("margin-right", "5px"); + $(button).css("top", 0); + $(button).css("right", 0); + $(button).css("transform", "translate(0, -50%)"); break; default: // top-left @@ -691,9 +699,12 @@ var tour = new function() { /** * Initialize the tour. */ - this.init = async function _in() { + this.init = async function _in(refresh=false) { try { tour.debug("initializing tour module"); + if (refresh) { + localStorage.removeItem("tour_state"); + } await tour.load_tour(); tour.post_init(); } catch (error) { diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index 26f0c11677e328c22eb70440772b3cb53bc4c146..2a4a803f95dd22f942d1f2d7ea9ee4b3303bf53a 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -288,10 +288,9 @@ </h5> </div> <!-- property value --> - <div class="col-sm-6 caosdb-property-value"> + <div class="col-sm-6 caosdb-f-property-value"> <xsl:apply-templates mode="property-value" select="."/> </div> - <div class="col-sm-6 caosdb-property-edit-value" style="display: none;"></div> <div class="col-sm-2 caosdb-property-edit" style="text-align: right;"></div> </div> </xsl:template> diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl index 3f4b268cb8c210012567196d1a6b2e25b2259940..907f27e512250cf5fde0ff2fe93fe85f800428a6 100644 --- a/src/core/xsl/navbar.xsl +++ b/src/core/xsl/navbar.xsl @@ -55,7 +55,7 @@ <span class="icon-bar"></span> <span class="icon-bar"></span> </button> - <a class="navbar-brand"> + <a class="navbar-brand" href="/"> <xsl:element name="img"> <xsl:if test="'${BUILD_NAVBAR_BRAND_NAME}' != ''"> <xsl:attribute name="class">caosdb-logo</xsl:attribute> diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl index 0015fe8d5beef5f1fc67c894b5b1421e2aaab4bb..8b6ae132b7b98118b03882c6bdeec259019c190b 100644 --- a/src/core/xsl/query.xsl +++ b/src/core/xsl/query.xsl @@ -195,7 +195,7 @@ <xsl:attribute name="data-property-name"> <xsl:value-of select="$field-name"/> </xsl:attribute> - <div class="caosdb-property-value caosdb-v-property-value"> + <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]"/> diff --git a/src/ext/js/fileupload.js b/src/ext/js/fileupload.js index 103af1914f2786ef6d8edd5987b04d6de215e2fc..fac11efadc3d1b6d364333d68b27904c37c3aa42 100644 --- a/src/ext/js/fileupload.js +++ b/src/ext/js/fileupload.js @@ -60,17 +60,18 @@ var fileupload = new function() { * The asyncronous error_handler function is an event listener to the * 'error' event of the XHR. and must accept an event parameter. * - * @param {object} dz_config {Object} - the configuration of the dropzone. - * @param {string} path - the default path. @param {function} - * success_handler - the async function which handles the server's respone - * in case of a 'success' http response. @param {function} error_handler - - * the async function which handles the server's respone in case of a - * 'error' http response. + * @param {object} dz_config- the configuration of the dropzone. + * @param {string} path - the default path. + * @param {function} success_handler - the async function which handles the + * server's respone in case of a 'success' http response. + * @param {function} error_handler - the async function which handles the + * server's respone in case of a 'error' http response. + * @param {string} atom_par - atomic datatype (not LIST<atom_par>). * * @return {HTMLElement} the bootstrap 'modal' */ this.create_fileupload_modal = function(dz_config, path, success_handler, - error_handler, par) { + error_handler, atom_par) { var modal = $(_modal_str); $(document.body).append(modal); @@ -82,9 +83,9 @@ var fileupload = new function() { var request = "<Request>"; for (const f of dropzone.getUploadingFiles()) { request = request + '<File upload="' + f.name + '" name="' + f.name + '" path="' + $("input#upload-path").val() + f.name + '">'; - if (typeof par !== "undefined" && par !== "FILE") { + if (typeof atom_par !== "undefined" && atom_par !== "FILE") { // add parent - request = request + '<Parent name="' + par + '" />'; + request = request + '<Parent name="' + atom_par + '" />'; } request = request + '</File>'; } @@ -104,6 +105,8 @@ var fileupload = new function() { }); modal.find(".caosdb-f-file-upload-submit-button").on("click", + // TODO call dropzone.processQueue when the save button of the + // edit_mode is pressed. function(e) { dropzone.processQueue(); }); @@ -125,7 +128,7 @@ var fileupload = new function() { this.create_success_handler = function(property) { // get property-value input element (in case of FILE property) - var input = $(property).find(".caosdb-property-edit-value input"); + var input = $(property).find(".caosdb-f-property-value input"); var set_value = function(entity) { input.val(getEntityId(entity)); } @@ -133,7 +136,7 @@ var fileupload = new function() { if (input.length == 0) { // no input means there is drop-down select input instead (this is // the case for other REFERENCE properties) - input = $(property).find(".caosdb-property-edit-value select"); + input = $(property).find(".caosdb-f-property-value select"); set_value = function(entity) { var option = $('<option selected="selected" value="' + getEntityID(entity) + '" >' + getEntityName(entity) + "</option>"); @@ -142,13 +145,13 @@ var fileupload = new function() { } /** - * Add the inserted files as value to the target property. + * Add the inserted files as value to the target property. Remove + * upload button. * * @param {Event} e - the 'load' event of the XHR. */ return async function(e) { var xhr = e.target; - console.log(xhr.responseXML); var array = await transformation.transformEntities(xhr.responseXML); if (array.len > 1) { @@ -162,6 +165,9 @@ var fileupload = new function() { input.after($(entity).find(".alert")); return; } + + // remove upload button + $(property).find(".caosdb-f-file-upload-button").remove(); set_value(entity); input.hide(); @@ -187,7 +193,7 @@ var fileupload = new function() { * @return {HTMLElement} a button element. */ this.create_small_icon_button = function() { - var button = $('<button class="btn btn-link navbar-btn" ><span class="glyphicon glyphicon-upload" aria-hidden="true"></span></button>'); + var button = $('<button class="caosdb-f-file-upload-button btn btn-link navbar-btn" ><span class="glyphicon glyphicon-upload" aria-hidden="true"></span></button>'); return button[0]; }; @@ -203,7 +209,7 @@ var fileupload = new function() { */ this.add_file_upload_button = function(target, button, func) { $(button).on("click", func); - $(target).prepend(button); + $(target).append(button); } /** @@ -243,6 +249,12 @@ var fileupload = new function() { fileupload.create_upload_app(e.target); }, true); + + // add global listener for data_type_changed event + document.body.addEventListener(edit_mode.property_data_type_changed.type, function(e) { + fileupload.create_upload_app(e.target); + }, true); + } this.debug = function(message) { @@ -260,15 +272,20 @@ var fileupload = new function() { this.create_upload_app = function(target) { const non_file_datatypes = ["TEXT", "DOUBLE", "BOOLEAN", "INTEGER", "DATETIME"]; var par = getPropertyDatatype(target); + var atom_par = par && par.startsWith("LIST<") && par.endsWith(">") ? par.substring(5, par.length-1) : par; - if (non_file_datatypes.indexOf(par) !== -1) { + if (non_file_datatypes.indexOf(atom_par) !== -1) { + return; + } + if (par != atom_par) { + // TODO implement for LIST<...> return; } var default_path = this.get_default_path(); var button = this.create_small_icon_button(); var success_handler = this.create_success_handler(target); var error_handler = this.create_error_handler(); - var edit_menu = $(target).find(".caosdb-property-edit")[0]; + var edit_menu = $(target).find(".caosdb-f-property-value")[0]; var dropzone_config = { "maxFiles": 1, @@ -278,7 +295,7 @@ var fileupload = new function() { default_path, success_handler, error_handler, - par); + atom_par); var toggle_function = function() { $(modal).modal() }; diff --git a/test/core/html/form_elements_example_1.html b/test/core/html/form_elements_example_1.html index d2d6fd34cd8e11f139d87f1cad6263fe482d72a8..aa10ac557cb9e015d08490bf8047a0e79a244407 100644 --- a/test/core/html/form_elements_example_1.html +++ b/test/core/html/form_elements_example_1.html @@ -193,7 +193,7 @@ </div> <div class="form-group caosdb-f-field caosdb-f-entity-property caosdb-f-form-field-required" data-field-name="cutting_date" data-groups="(part2)(part3)" style=""> <label class="control-label col-sm-3" data-property-name="cutting_date" for="cutting_date">Cutting Date</label> - <div class="caosdb-property-value col-sm-9"> + <div class="caosdb-f-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="cutting_date" type="date"/> </div> </div> @@ -201,13 +201,13 @@ <label class="control-label col-sm-3" data-property-name="bag_numbers" for="bag_numbers">Bag Numbers</label> <div class="caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers_from"> <label class="control-label col-sm-1" data-property-name="bag_numbers_from" for="bag_numbers_from">from</label> - <div class="caosdb-property-value col-sm-3"> + <div class="caosdb-f-property-value col-sm-3"> <input class="form-control caosdb-property-text-value" name="bag_numbers_from" step="1" type="number"/> </div> </div> <div class="caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers_to"> <label class="control-label col-sm-1 col-sm-offset-1" data-property-name="bag_numbers_to" for="bag_numbers_to">to</label> - <div class="caosdb-property-value col-sm-3"> + <div class="caosdb-f-property-value col-sm-3"> <input class="form-control caosdb-property-text-value" name="bag_numbers_to" step="1" type="number"/> </div> </div> @@ -334,25 +334,25 @@ <fieldset class="form-inline caosdb-f-form-elements-subform" data-subform-name="new_subsamples" id="new_subsamples_6335" style="padding-left: 15px; padding-right: 15px;"> <legend>Subsample</legend> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="type"> - <div class="caosdb-property-value col-sm-9"> + <div class="caosdb-f-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="type" type="hidden" value="6335"/> </div> </div> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="width" for="width">width (cm)</label> - <div class="caosdb-property-value"> + <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="width" step="any" type="number"/> </div> </div> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="height" for="height">height (cm)</label> - <div class="caosdb-property-value"> + <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="height" step="any" type="number"/> </div> </div> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="rectangular" for="rectangular">rectangular</label> - <div class="caosdb-property-value"> + <div class="caosdb-f-property-value"> <input class="caosdb-property-text-value" name="rectangular" type="checkbox"/> </div> </div> @@ -360,25 +360,25 @@ <fieldset class="form-inline caosdb-f-form-elements-subform" data-subform-name="new_subsamples" id="new_subsamples_6338" style="padding-left: 15px; padding-right: 15px;"> <legend>PP_Sample</legend> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="type"> - <div class="caosdb-property-value col-sm-9"> + <div class="caosdb-f-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="type" type="hidden" value="6338"/> </div> </div> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="width" for="width">width (cm)</label> - <div class="caosdb-property-value"> + <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="width" step="any" type="number"/> </div> </div> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="height" for="height">height (cm)</label> - <div class="caosdb-property-value"> + <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="height" step="any" type="number"/> </div> </div> <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> <label class="control-label" data-property-name="rectangular" for="rectangular">rectangular</label> - <div class="caosdb-property-value"> + <div class="caosdb-f-property-value"> <input class="caosdb-property-text-value" name="rectangular" type="checkbox"/> </div> </div> diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index 1ec57f31d7da949e6310958ab4516206e073d811..61058a4301cc43c6f6da56999bcfccd320d92424 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -252,7 +252,7 @@ QUnit.test("smooth_replace", function(assert){ QUnit.test("make_property_editable", function(assert) { assert.ok(edit_mode.make_property_editable); - assert.throws(() => edit_mode.make_property_editable(undefined), /parameter `element` was undefined./, "undefined"); + assert.throws(() => edit_mode.make_property_editable(undefined), /param 'element' is expected to be an HTMLElement, was undefined/, "undefined"); // test for correct parsing of datatypes @@ -463,3 +463,357 @@ QUnit.test("remove_delete_button", function(assert){ } +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) { + assert.timeout(100000); + // unique dummy values for each data type + var data = { + "TEXT": "Single", + "DATETIME": "1996-12-15", + "DOUBLE": "4.5", + "INTEGER": "111", + "BOOLEAN": "TRUE", + "FILE": "1234", + "REFERENCE": "3456", + "Person": "5678", + } + + for (let dt of ["BOOLEAN", "DATETIME", "TEXT", "Person", "DOUBLE", "FILE", "REFERENCE", "INTEGER"]) { + var prop = { + datatype: dt, + reference: ["Person", "REFERENCE", "FILE"].includes(dt), + value: data[dt], + } + + var html = $(edit_mode.createElementForProperty(prop)); + + assert.equal($(html).val(), data[dt], `${dt} input has correct value`); + + } +}); + +QUnit.test("getPropertyFromElement", async function(assert) { + assert.timeout(100000); + // unique dummy values for each data type + var data = { + "TEXT": "Single", + "LIST<TEXT>": "One", + "DATETIME": "1996-12-15", + "LIST<DATETIME>": "2020-01-01T20:15:18", + "DOUBLE": "4.5", + "LIST<DOUBLE>": "5.5", + "INTEGER": "111", + "LIST<INTEGER>": "222", + "BOOLEAN": "TRUE", + "LIST<BOOLEAN>": "FALSE", + "FILE": "1234", + "LIST<FILE>": "2345", + "REFERENCE": "3456", + "LIST<REFERENCE>": "4567", + "Person": "5678", + "LIST<Person>": "6789", + } + for (let dt of ["BOOLEAN", "DATETIME", "TEXT", "Person", "DOUBLE", "FILE", "REFERENCE", "INTEGER"]) { + var non_list_prop_xml_str = `<Property unit="m-${dt}" datatype="${dt}">${data[dt]}</Property>`; + var list_prop_xml_str = `<Property unit="kg-${dt}" datatype="LIST<${dt}>"><Value>${data["LIST<" + dt + ">"]}</Value></Property>`; + var long_list_prop_xml_str = `<Property unit="s-${dt}" datatype="LIST<${dt}>"><Value>${data[dt]}</Value><Value>${data[dt]}</Value></Property>`; + + var non_list_prop_html = await transformProperty(non_list_prop_xml_str); + var list_prop_html = await transformProperty(list_prop_xml_str); + var long_list_prop_html = await transformProperty(long_list_prop_xml_str); + + var non_list_pre = getPropertyFromElement(non_list_prop_html); + assert.equal(non_list_pre.value, data[dt], "pre has value"); + + edit_mode.make_property_editable(non_list_prop_html); + edit_mode.make_property_editable(list_prop_html); + edit_mode.make_property_editable(long_list_prop_html); + + var non_list_prop = edit_mode.getPropertyFromElement(non_list_prop_html); + var list_prop = edit_mode.getPropertyFromElement(list_prop_html); + var long_list_prop = edit_mode.getPropertyFromElement(long_list_prop_html); + + if (dt == "DOUBLE" || dt == "INTEGER") { + assert.equal(non_list_prop.unit, `m-${dt}`, "non-list property has correct unit"); + assert.equal(list_prop.unit, `kg-${dt}`, "list property has correct unit"); + assert.equal(long_list_prop.unit, `s-${dt}`, "long list property has correct unit"); + } + + assert.equal(non_list_prop.datatype, dt, `${dt} non list has correct datatype`); + assert.equal(list_prop.datatype, `LIST<${dt}>`, `${dt} list has correct datatype`); + assert.propEqual(long_list_prop.datatype, `LIST<${dt}>`, `${dt} long list has correct datatype`); + + assert.equal(non_list_prop.value, data[dt], `${dt} non list has correct value`); + assert.equal(list_prop.value, data[`LIST<${dt}>`], `${dt} list has correct value`); + assert.propEqual(long_list_prop.value, [data[dt], data[dt]], `${dt} long list has correct values`); + + + } + +}); + + +/** + * The _toggle_list_property_object method converts a property object which represents + * a non-list property into one that presents a list property and vice-versa. + * + * This test case performs a large set of different data types. + */ +QUnit.test("_toggle_list_property_object", async function(assert) { + + // unique dummy values for each data type + var data = { + "TEXT": "Single", + "LIST<TEXT>": "One", + "DATETIME": "1996-12-15", + "LIST<DATETIME>": "2020-01-01T20:15:18", + "DOUBLE": "4.5", + "LIST<DOUBLE>": "5.5", + "INTEGER": "111", + "LIST<INTEGER>": "222", + "BOOLEAN": "TRUE", + "LIST<BOOLEAN>": "FALSE", + "FILE": "1234", + "LIST<FILE>": "2345", + "REFERENCE": "3456", + "LIST<REFERENCE>": "4567", + "Person": "5678", + "LIST<Person>": "6789", + } + for (let dt of ["DATETIME", "TEXT", "Person", "DOUBLE", "FILE", "REFERENCE", "INTEGER", "BOOLEAN"]) { + var non_list_prop_xml_str = `<Property unit="m-${dt}" datatype="${dt}">${data[dt]}</Property>`; + var list_prop_xml_str = `<Property unit="kg-${dt}" datatype="LIST<${dt}>"><Value>${data["LIST<" + dt + ">"]}</Value></Property>`; + var long_list_prop_xml_str = `<Property unit="s-${dt}" datatype="LIST<${dt}>"><Value>${data[dt]}</Value><Value>${data[dt]}</Value></Property>`; + + var non_list_prop_html = await transformProperty(non_list_prop_xml_str); + var list_prop_html = await transformProperty(list_prop_xml_str); + var long_list_prop_html = await transformProperty(long_list_prop_xml_str); + + edit_mode.make_property_editable(non_list_prop_html); + edit_mode.make_property_editable(list_prop_html); + edit_mode.make_property_editable(long_list_prop_html); + + assert.notOk(edit_mode._toggle_list_property_object(non_list_prop_html, false), "no changes"); + assert.notOk(edit_mode._toggle_list_property_object(list_prop_html, true), "no changes"); + + var list_inputs_property = edit_mode._toggle_list_property_object(non_list_prop_html, true); + var single_input_property = edit_mode._toggle_list_property_object(list_prop_html, false); + + assert.equal(single_input_property.value, data[`LIST<${dt}>`], "list->single has correct value"); + if (dt == "INTEGER" || dt == "DOUBLE") { + assert.equal(single_input_property.unit, `kg-${dt}`, "list -> single has correct unit"); + } + assert.equal(single_input_property.list, false, "list->single is now a single property"); + assert.equal(single_input_property.datatype, dt, "list->single has correct datatype"); + assert.notOk(single_input_property.listDatatype, "list->single has correct listDatatype (undefined)."); + assert.propEqual(list_inputs_property.value, [data[dt]], "single->list has correct value"); + if (dt == "INTEGER" || dt == "DOUBLE") { + assert.equal(list_inputs_property.unit, `m-${dt}`, "single -> list has correct unit"); + } + assert.equal(list_inputs_property.list, true, "single->list is now a list"); + assert.equal(list_inputs_property.datatype, `LIST<${dt}>`, "single->list has correct datatype"); + 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"); + + } +}); + + +/** + * _toggle_list_property converts a HTML representation (in edit_mode) of a non-list + * property into a list property and vice versa. It internally uses + * _toggle_list_property_object: HTML -> JS Object -> Converted JS Object -> + * Converted HTML. + */ +QUnit.test("_toggle_list_property", async function(assert) { + + // unique dummy values for each data type + var data = { + "TEXT": "Single", + "LIST<TEXT>": "One", + "DATETIME": "1996-12-15", + "LIST<DATETIME>": "2020-01-01T20:15:19", + "DOUBLE": "4.5", + "LIST<DOUBLE>": "5.5", + "INTEGER": "111", + "LIST<INTEGER>": "222", + "BOOLEAN": "TRUE", + "LIST<BOOLEAN>": "FALSE", + "FILE": "1234", + "LIST<FILE>": "2345", + "REFERENCE": "3456", + "LIST<REFERENCE>": "4567", + "Person": "5678", + "LIST<Person>": "6789", + } + for (let dt of ["DATETIME", "BOOLEAN", "TEXT", "DOUBLE", "FILE", "REFERENCE", "Person", "INTEGER"]) { + var non_list_prop_xml_str = `<Property unit="m-${dt}" datatype="${dt}">${data[dt]}</Property>`; + var list_prop_xml_str = `<Property unit="kg-${dt}" datatype="LIST<${dt}>"><Value>${data["LIST<" + dt + ">"]}</Value></Property>`; + var long_list_prop_xml_str = `<Property unit="s-${dt}" datatype="LIST<${dt}>"><Value>${data[dt]}</Value><Value>${data[dt]}</Value></Property>`; + + var non_list_prop_html = await transformProperty(non_list_prop_xml_str); + var list_prop_html = await transformProperty(list_prop_xml_str); + var long_list_prop_html = await transformProperty(long_list_prop_xml_str); + + edit_mode.make_property_editable(non_list_prop_html); + edit_mode.make_property_editable(list_prop_html); + edit_mode.make_property_editable(long_list_prop_html); + + assert.equal($(non_list_prop_html).find(".caosdb-f-property-value").length, 1); + + assert.notOk(edit_mode._toggle_list_property(non_list_prop_html, false), "no changes"); + assert.notOk(edit_mode._toggle_list_property(list_prop_html, true), "no changes"); + + 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.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") { + 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`); + } + + +}); + + +/** + * Tests the buggy behavior of retrieve_datatype_list and + * fill_reference_drop_down. The bug deleted the value of reference properties + * and it had a two-fold cause: + * + * Firstly, properties with plain `REFERENCE` or `LIST<REFERENCE>` data types + * (or 'FILE`) would no construct a correct query but the executed a `FIND + * Record REFERENCE` which always returns an emtpy result set as options. + * + * Then, an empty set of options would produce a completely empty drop-down menu + * (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) { + var query_done; + edit_mode.query = function (query) { + var re = /^FIND (Record|File)\s*$/g; + assert.ok(query.match(re), `${query} should match ${re}`); + query_done(); + return []; + } + + // retrieve_datatype_list calls edit_mode.query with correct query string. + query_done = assert.async(2); + await edit_mode.retrieve_datatype_list("REFERENCE"); + query_done = assert.async(1); // only called with file + await edit_mode.retrieve_datatype_list("FILE"); + + // old option not deleted when options are empty + var resolve_function; + var empty_options = new Promise(function(res, err) { + resolve_function = res; + }); + var property = $("<div/>") + .append(edit_mode.createElementForProperty({ + reference: true, + datatype: "REFERENCE", + list: false, + value: "1234" + }, 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"); + + // mimic retrieve_datatype_list with empty result + resolve_function([]); + + assert.equal($(property).find("select").val(), "1234", "old value after"); + assert.equal($(property).find("option").length, 1, "one option after"); + assert.equal($(property).find(":selected").text(), "1234", "old text after"); + + + // now an integration test. A list of options is passed to + var options = edit_mode._create_reference_options(await transformation + .transformEntities(str2xml(` + <Response> + <Record name="RName1" id="RID1"/> + <Record name="RName2" id="RID2"/> + <Record name="RName1234" id="1234"/> + </Response> + `))); + + 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) { + await proxied(arg1, arg2); + + assert.equal($(property).find("select").val(), "1234", "still old value after"); + assert.equal($(property).find("option").length, 3, "3 options after"); + assert.equal($(property).find(":selected").text(), "Name: RName1234, CaosDB ID: 1234", "new text after"); + fill_method_done(); + } + + property = $("<div/>") + .append(edit_mode.createElementForProperty({ + reference: true, + datatype: "REFERENCE", + list: false, + value: "1234" + }, options)); + + + edit_mode.fill_reference_drop_down = proxied; + +}); + + +QUnit.test("fill_reference_drop_down", async function(assert) { + var options = edit_mode._create_reference_options(await transformation + .transformEntities(str2xml(` + <Response> + <Record name="RName1" id="RID1"/> + <Record name="RName2" id="RID2"/> + <Record name="RName1234" id="1234"/> + </Response> + `))); + + assert.equal(options.length, 3, "3 entities returned"); + assert.ok($(options).is("option"), "options contains options"); + var select = $('<select><option class="caosdb-f-option-default" selected value="1234">1234</option></select>'); + assert.equal(select.find("option").length, 1, "one option before"); + assert.equal(select.val(), "1234", "old value before"); + assert.equal(select.find(":selected").text(), "1234", "oldtext before"); + + await edit_mode.fill_reference_drop_down(select[0], options); + + assert.equal(select.find("option").length, 3, "3 options after"); + assert.equal(select.val(), "1234", "old value after"); + assert.equal(select.find(":selected").text(), "Name: RName1234, CaosDB ID: 1234", "new text after"); + +}); + + +/** + * Test the inner logic of retrieve_datatype_list. + */ +QUnit.test("_create_reference_options", async function(assert) { + var entities = await transformation + .transformEntities(str2xml(` + <Response> + <Record name="RName" id="RID"/> + </Response> + `)); + assert.equal(edit_mode._create_reference_options(entities)[0].value, "RID"); + assert.equal(edit_mode._create_reference_options(entities)[0].innerHTML, "Name: RName, CaosDB ID: RID"); +}); diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 972691364c5bfd402313e6b3ad8fd29ed572b4ed..9d068ad1c688c7a8643c18846103ea33a10c8874 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -33,12 +33,12 @@ QUnit.module("ext_map.js", { <div class="list-group-item caosdb-f-entity-property"> <div class="caosdb-property-name">` + lat + `</div> - <div class="caosdb-property-value">1.23</div> + <div class="caosdb-f-property-value">1.23</div> </div> <div class="list-group-item caosdb-f-entity-property"> <div class="caosdb-property-name">` + lng + `</div> - <div class="caosdb-property-value">5.23</div> + <div class="caosdb-f-property-value">5.23</div> </div> </div>`; }, diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js index b45a3deb99000dd173c755f7d5f0c9e32f4e24f1..9c64a78826f033378cc92f79510399f44023d8b8 100644 --- a/test/core/js/modules/ext_references.js.js +++ b/test/core/js/modules/ext_references.js.js @@ -60,12 +60,12 @@ QUnit.test("get_person_str", function(assert){ QUnit.test("update_visible_references", async function(assert){ const f = resolve_references.update_visible_references; - const test_property = $(`<div class="caosdb-property-value"> + const test_property = $(`<div class="caosdb-f-property-value"> <div data-entity-id="15" class="${resolve_references._unresolved_class_name}"> <span class="${resolve_references._target_class}"/></span> </div> - </div><div class="caosdb-property-value"> + </div><div class="caosdb-f-property-value"> <div class="caosdb-value-list"> <div data-entity-id="16" class="${resolve_references._unresolved_class_name}"> diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 874b68e137121ac886082212cda564ec53da3005..9ab5c5a3f5997d4dc9165466ed117fafaa25b7fc 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -512,7 +512,7 @@ QUnit.test("createHidePreviewButton", function(assert) { QUnit.test("addHidePreviewButton", function(assert) { assert.ok(preview.addHidePreviewButton, "function available"); - let okTestElem = $('<div><div class="caosdb-property-value"></div></div>')[0] + let okTestElem = $('<div><div class="caosdb-f-property-value"></div></div>')[0] let notOkTestElem = $('<div></div>')[0] assert.throws(() => { @@ -537,7 +537,7 @@ QUnit.test("addHidePreviewButton", function(assert) { QUnit.test("addShowPreviewButton", function(assert) { assert.ok(preview.addShowPreviewButton, "function available"); - let okTestElem = $('<div><div class="caosdb-property-value"></div></div>')[0] + let okTestElem = $('<div><div class="caosdb-f-property-value"></div></div>')[0] let notOkTestElem = $('<div></div>')[0] assert.throws(() => { @@ -650,7 +650,7 @@ QUnit.test("getRefLinksContainer", function(assert) { // TODO: references or lists of references should have a special class, not just // caosdb-value-list. -> entity.xsl let okElem = $('<div><div class="caosdb-value-list"><a><span class="caosdb-id">1234</span></a></div></div>')[0]; - let okSingle = $('<div><div class="caosdb-property-value"><a class="btn"><span class="caosdb-id">1234</span></a></div><</div>')[0]; + let okSingle = $('<div><div class="caosdb-f-property-value"><a class="btn"><span class="caosdb-id">1234</span></a></div><</div>')[0]; let notOkElem = $('<div></div>')[0]; assert.throws(() => { @@ -975,7 +975,7 @@ QUnit.test("createCarouselNav", function(assert) { }); let xmlResponse = str2xml('<Response><Record id="1234"/><Record id="2345"/><Record id="3456"/><Record id="4567"/></Response>'); - let ref_property_elem = $('<div><div class="caosdb-property-value"></div></div>'); + let ref_property_elem = $('<div><div class="caosdb-f-property-value"></div></div>'); let original_get = connection.get; ref_property_elem.find('div').append(refLinks);