diff --git a/CHANGELOG.md b/CHANGELOG.md index acb0bb1514d07198e7898cf3e3e2d681239796c6..38ddc9b75626ed3cbe4e3a3dd9043b4372228258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features, dependecies etc.) +* Visually highlighted drop zones for properties and parents in the edit_mode. * two new field types for the form_elements module, `file` and `select`. See the module documentation for more information. @@ -19,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Several minor cosmetic flaws - Fixed edit mode for Safari 11. ### Security (in case of vulnerabilities) diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index 69a700376423a44bcb28a9920f1f3d15ef9a3b90..c4329eb20c788acb933b6b9f8d4476019ece6df4 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -27,6 +27,14 @@ body { flex-direction: column; } +html { + min-height: 100vh%; + background-color: lightgray; +} + +.caosdb-v-server-message strong { + margin-right: 8px; +} div.export-data { display: none; @@ -169,6 +177,7 @@ button.caosdb-v-entity-version-button { .caosdb-f-main { display: flex; width: unset; + min-height: 65vh; } .caosdb-f-main-entities { width: calc(100% - 5px); @@ -618,6 +627,74 @@ input[type="file"] { min-height: 22px; } +.caosdb-v-property-value-inputs > textarea { + width: 100%; +} + +.caosdb-v-property-value-inputs li > textarea { + width: calc(100% - 40px); +} + +.caosdb-v-edit-mode-property-dropzone { + list-style: none; + text-align: center; + color: #69c2df; + border: 2px dashed #69c2df; + padding: 25px 0px; + margin: 25px 0px; +} + +.caosdb-v-edit-mode-property-dropzone:hover { + filter: brightness(80%); +} + +.caosdb-v-edit-mode-parent-dropzone { + position: relative; + display: block; + color: #69c2df; + border: 2px dashed #69c2df; + padding-top: 15px; + padding-bottom: 15px; + padding-left: 5px; + padding-right: 15px; + margin: 0; +} + +.caosdb-v-edit-mode-parent-dropzone:hover { + filter: brightness(80%); +} + +.caosdb-v-edit-mode-highlight { + color: #333; + background-color: #d3fdd3; + border: 2px solid #d3fdd3; +} + +.caosdb-v-edit-mode-parent-dropzone div:first-child { + font-size: 80%; + position: absolute; + top: 0px; + right: 0px; + margin: 0px; +} + +.caosdb-v-property-value-inputs .caosdb-v-edit-value-list-buttons > button { + padding: 1px; +} + +.caosdb-v-property-other-inputs * + * { + margin-left: 6px; +} + +.caosdb-v-property-other-inputs label * { + margin-left: 6px; +} + +.caosdb-v-property-other-inputs { + margin-top: 10px; + margin-bottom: 10px; +} + footer { background-color: lightgrey; padding: 0.5em; diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index 5da4303fb7227e12b38ca2d23628e21a4c996c63..c37dcb1b62b75e5e2078ffb10670288441d2be07 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -106,34 +106,47 @@ var edit_mode = new function() { if (typeof new_prop === "undefined" || !(new_prop instanceof HTMLElement)) { throw new TypeError("new_prop must instantiate HTMLElement"); } - var rt = entity.getElementsByClassName("caosdb-properties")[0]; - rt.appendChild(new_prop); + const drop_zone = $(entity).find(".caosdb-properties").find(".caosdb-f-edit-mode-property-dropzone"); + drop_zone.before(new_prop); make_property_editable_cb(new_prop); new_prop.dispatchEvent(edit_mode.property_added); } - this.add_dropped_property = function(e, panel) { + + /** + * Add a dropped property to the entity. + * + * @param {Event} e - the drop event. + * @param {HTMLElement} entity - the entity. + */ + this.add_dropped_property = function(e, entity) { var propsrcid = e.dataTransfer.getData("text/plain"); var tmp_id = propsrcid.split("-"); var prop_id = tmp_id[tmp_id.length - 1]; var entity_type = tmp_id[tmp_id.length - 2]; if (entity_type == "p") { retrieve_dragged_property(prop_id).then(new_prop_doc => { - edit_mode.add_new_property(panel, new_prop_doc.firstChild); + edit_mode.add_new_property(entity, new_prop_doc.firstChild); }, edit_mode.handle_error); } else if (entity_type == "rt") { var name = $("#" + propsrcid).text(); var dragged_rt = str2xml('<Response><Property id="' + prop_id + '" name="' + name + '" datatype="' + name + '"></Property></Response>'); transformation.transformProperty(dragged_rt).then(new_prop_doc => { - edit_mode.add_new_property(panel, new_prop_doc.firstChild); + edit_mode.add_new_property(entity, new_prop_doc.firstChild); }, edit_mode.handle_error); } } - this.add_dropped_parent = function(e, panel) { + /** + * Add a dropped parent to the entity. + * + * @param {Event} e - the drop event. + * @param {HTMLElement} entity - the entity. + */ + this.add_dropped_parent = function(e, entity) { var propsrcid = e.dataTransfer.getData("text/plain"); - var parent_list = panel.getElementsByClassName("caosdb-f-parent-list")[0] + var parent_list = entity.getElementsByClassName("caosdb-f-parent-list")[0] var tmp_id = propsrcid.split("-"); var prop_id = tmp_id[tmp_id.length - 1]; var entity_type = tmp_id[tmp_id.length - 2]; @@ -150,14 +163,14 @@ var edit_mode = new function() { is_parent=true ); */ - edit_mode.add_parent_delete_buttons(panel); + edit_mode.add_parent_delete_buttons(entity); }, edit_mode.handle_error); } } this.property_drop_listener = function(e) { - edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); + edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); } this.parent_drop_listener = function(e) { @@ -527,12 +540,12 @@ var edit_mode = new function() { return edit_mode_li[0]; } - this.toggle_edit_mode = function() { + this.toggle_edit_mode = async function() { edit_mode.toggle_edit_panel(); if (edit_mode.is_edit_mode()) { - edit_mode.leave_edit_mode(); + await edit_mode.leave_edit_mode(); } else { - edit_mode.enter_edit_mode(); + await edit_mode.enter_edit_mode(); } } @@ -543,15 +556,15 @@ var edit_mode = new function() { this.leave_edit_mode = function() {} - this.enter_edit_mode = function(editApp = undefined) { + this.enter_edit_mode = async function(editApp = undefined) { window.localStorage.edit_mode = "true"; var editPanel = edit_mode.get_edit_panel(); removeAllWaitingNotifications(editPanel); this.add_wait_datamodel_info(); - // TODO make enter_edit_mode ayncronous? - return edit_mode.retrieve_data_model().then(model => { + try { + const model = await edit_mode.retrieve_data_model(); $(".caosdb-f-btn-toggle-edit-mode").text("Leave Edit Mode"); edit_mode.init_tool_box(model); @@ -567,7 +580,9 @@ var edit_mode = new function() { }; return nextEditApp; - }, edit_mode.handle_error); + } catch (err) { + edit_mode.handle_error(err); + } } @@ -650,6 +665,7 @@ var edit_mode = new function() { header.children().remove(); const form = $('<form class="form-horizontal"></form>').append(inputs); header.append(form); + edit_mode.add_parent_dropzone(entity); edit_mode.make_datatype_input_logic(form[0]); edit_mode.add_parent_delete_buttons(header[0]); @@ -839,7 +855,7 @@ var edit_mode = new function() { this.createElementForProperty = function(property, options) { var result; if (property.datatype == "TEXT") { - result = "<textarea>" + ( property.value || "" ) + "</textarea>"; + result = `<textarea>${property.value || ""}</textarea>`; } else if (property.datatype == "DATETIME") { var dateandtime = [""]; if(property.value) { @@ -912,7 +928,9 @@ var edit_mode = new function() { this.parentElement.parentElement.parentElement.dispatchEvent(edit_mode.list_value_input_added); }); - return $("<span></span>").append(deleteButton).append(insertButton)[0]; + return $('<span class="caosdb-v-edit-value-list-buttons"></span>') + .append(deleteButton) + .append(insertButton)[0]; } @@ -1095,9 +1113,9 @@ var edit_mode = new function() { */ this.add_toggle_list_checkbox = function (element, list, datatype) { var editfield = $(element).find(".caosdb-f-property-value"); - var label = "List "; + var label = $('<label>List</label>'); var checkbox = $('<input type="checkbox" class="caosdb-f-entity-is-list"/>'); - $(element).find(".caosdb-property-edit").prepend(checkbox).prepend(label); + $(element).find(".caosdb-property-edit").prepend(label.append(checkbox)); checkbox.prop("checked", list); @@ -1162,7 +1180,11 @@ var edit_mode = new function() { this.make_property_editable = function(element) { caosdb_utils.assert_html_element(element, "param 'element'"); - var editfield = $(element).find(".caosdb-f-property-value"); + var editfield = $(element).find(".caosdb-f-property-value") + .removeClass("col-sm-8") + .addClass("col-sm-6") + .addClass("caosdb-v-property-value-inputs") + .after(`<div class="col-sm-2 caosdb-v-property-other-inputs caosdb-property-edit" style="text-align: right;"/>`); var property = getPropertyFromElement(element); @@ -1487,6 +1509,9 @@ var edit_mode = new function() { for (var element of prop_elements) { edit_mode.make_property_editable(element); } + if(getEntityRole(app.entity) != "Property") { + edit_mode.add_property_dropzone(app.entity); + } app.entity.dispatchEvent(edit_mode.start_edit); } app.onEnterWait = function(e) { @@ -1580,6 +1605,18 @@ var edit_mode = new function() { }); } + this.add_property_dropzone = function (entity) { + $(entity).find("ul.caosdb-properties") + .append('<li class="caosdb-v-edit-mode-dropzone caosdb-f-edit-mode-property-dropzone caosdb-v-edit-mode-property-dropzone">Drag and drop Properties and RecordTypes from the Edit Mode Toolbox here.</li>'); + } + + this.add_parent_dropzone = function (entity) { + $(entity).find(".caosdb-f-parent-list") + .addClass("caosdb-v-edit-mode-parent-dropzone") + .addClass("caosdb-v-edit-mode-dropzone") + .prepend('<div>Drag and drop RecordTypes from the Edit Mode Toolbox here.</div>'); + } + this.unfreeze = function() { $('.caosdb-f-main-entities').children().each(function(index) { edit_mode.unfreeze_entity(this); @@ -1665,11 +1702,13 @@ var edit_mode = new function() { } this.highlight = function(entity) { - $(entity).addClass("caosdb-v-edit-mode-highlight").css("background-color", "lightgreen"); + $(entity).find(".caosdb-v-edit-mode-dropzone") + .addClass("caosdb-v-edit-mode-highlight"); } this.unhighlight = function() { - $('.caosdb-v-edit-mode-highlight').removeClass("caosdb-v-edit-mode-highlight").css("background-color", ""); + $('.caosdb-v-edit-mode-highlight') + .removeClass("caosdb-v-edit-mode-highlight"); } this.handle_error = function(err) { @@ -1766,8 +1805,48 @@ var edit_mode = new function() { } } + /** + * List of all permissions which indicate that the edit button should be + * visible. + */ + const UPDATE_PERMISSIONS = [ + "UPDATE:DESCRIPTION", + "UPDATE:VALUE", + "UPDATE:ROLE", + "UPDATE:PARENT:REMOVE", + "UPDATE:PARENT:ADD", + "UPDATE:PROPERTY:REMOVE", + "UPDATE:PROPERTY:ADD", + "UPDATE:NAME", + "UPDATE:DATA_TYPE", + "UPDATE:FILE:REMOVE", + "UPDATE:FILE:ADD", + "UPDATE:FILE:MOVE", + "UPDATE:QUERY_TEMPLATE_DEFINITION", + ]; + /** + * Add a button labeled "Edit" to the entity which opens the edit form for + * this entity. + * + * The button is added only when any of the `UPDATE:...` permissions are + * there. + * + * @param {HTMLElement} entity - the entity which gets the button. + * @parma {function} callback - the function which initializes and opens + * the edit form. + */ this.add_start_edit_button = function(entity, callback) { + var has_any_update_permission = false; + for (let permission of UPDATE_PERMISSIONS) { + if (hasEntityPermission(entity, permission)) { + has_any_update_permission = true; + break; + } + } + if (!has_any_update_permission) { + return; + } edit_mode.remove_start_edit_button(entity); var button = $('<button title="Edit this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-start-edit-button">Edit</button>'); @@ -1782,6 +1861,9 @@ var edit_mode = new function() { this.add_new_record_button = function(entity, callback) { + if (!hasEntityPermission(entity, "USE:AS_PARENT")) { + return; + } edit_mode.remove_new_record_button(entity); var button = $('<button title="Create a new Record from this RecordType." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-new-record-button">+Record</button>'); @@ -1795,6 +1877,9 @@ var edit_mode = new function() { } this.add_delete_button = function(entity, callback) { + if (!hasEntityPermission(entity, "DELETE")) { + return; + } edit_mode.remove_delete_button(entity); var button = $('<button title="Delete this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-delete-button">Delete</button>'); diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index cac9e87ea40bf6a687a19a1942b5534276c8faf7..21b38feedb92de8feebb75e15f502d5181a078fe 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -268,10 +268,9 @@ </h5> </div> <!-- property value --> - <div class="col-sm-6 caosdb-f-property-value"> + <div class="col-sm-8 caosdb-f-property-value"> <xsl:apply-templates mode="property-value" select="."/> </div> - <div class="col-sm-2 caosdb-property-edit" style="text-align: right;"></div> </div> </xsl:template> <xsl:template name="single-value"> diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl index e414f9af4147714c5f6ff8a03b70309a02ec1446..4d4df12a667c0c119e69e714ce0078b690f6457d 100644 --- a/src/core/xsl/main.xsl +++ b/src/core/xsl/main.xsl @@ -210,6 +210,11 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/edit_mode.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_entity_state.js')"/> + </xsl:attribute> + </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_file_download.js')"/> diff --git a/src/core/xsl/messages.xsl b/src/core/xsl/messages.xsl index 392d6e37e51e7426feafff8726382abb4012376b..1ac2e7b63c16bd389e9a6e0fe0c2f9541be34683 100644 --- a/src/core/xsl/messages.xsl +++ b/src/core/xsl/messages.xsl @@ -26,7 +26,7 @@ <xsl:template match="Error|Warning|Info"> <xsl:param name="class"/> <div> - <xsl:attribute name="class">alert + <xsl:attribute name="class">alert caosdb-v-server-message <xsl:value-of select="$class"/> alert-dismissable fade in</xsl:attribute> <a class="close" data-dismiss="alert" href="#"> <xsl:value-of select="$close-char"/> diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index 2d7a605fd29788d189d2af0d828266e7e3a35f84..8fde3bac9ce33742c5d79ca9e5ca03e2d0e0d094 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -93,9 +93,9 @@ QUnit.test("add_new_property", function (assert) { var done = assert.async(2); // test case setup - var entity = $("<div><div class='caosdb-properties' /></div>")[0]; + var entity = $(`<div><ul class='caosdb-properties'/><li class="caosdb-f-entity-property"><ol><li>value1</li></ol></li><li class="caosdb-f-edit-mode-property-dropzone"></li></ul>`)[0]; $(document.body).append(entity); - var new_prop = $("<div id='test_new_prop'/>")[0]; + var new_prop = $("<div class='test_new_prop'/>")[0]; // test bad cases assert_throws(assert, () => { @@ -113,8 +113,8 @@ 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.equal($(entity).find(".test_new_prop").length, 0, "no property"); + entity.addEventListener(edit_mode.property_added.type, function (e) { assert.ok(e.target === new_prop, "event fired on newprop"); assert.ok(this === entity, "event detected on entity"); done(); @@ -124,7 +124,7 @@ QUnit.test("add_new_property", function (assert) { "make_property_editable_cb called"); done(); }); - assert.equal($(entity).find("#test_new_prop").length, 1, "one property"); + assert.equal($(entity).find(".test_new_prop").length, 1, "one property"); // event has been fired and property has been added. $(entity).remove();