diff --git a/CHANGELOG.md b/CHANGELOG.md index d3689e84e60ae5e3b37ac21425e486bff9a0e6cf..420d4b5112295eec4b3485203a6f2e4be246624a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Module `ext_qrcode` which generates a QR Code for an entity (pointing to the the head or the exact version). +* Optional functionality to bookmark all query results. Note that too many + bookmarks will result in the URI being too lang and bookmarks will have to be + cleared manually. ### Changed (for changes in existing functionality) diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 412773eb86d46151d1126d29c5e539439d68a6fe..65a2b9bc42f9e0f22dc0cd6ca6baac094e94eee7 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -49,6 +49,7 @@ BUILD_MODULE_EXT_BOTTOM_LINE=ENABLED BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW=DISABLED BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW=DISABLED BUILD_MODULE_EXT_BOOKMARKS=ENABLED +BUILD_MODULE_EXT_ADD_QUERY_TO_BOOKMARKS=DISABLED BUILD_MODULE_EXT_ANNOTATION=ENABLED BUILD_MODULE_EXT_COSMETICS_LINKIFY=DISABLED BUILD_MODULE_EXT_QRCODE=ENABLED diff --git a/src/core/js/ext_bookmarks.js b/src/core/js/ext_bookmarks.js index d99ff362d8a26ee4818a76a624c80f55f757c419..d9da463699ff7021b9f94a877aa3d9379dffb5dc 100644 --- a/src/core/js/ext_bookmarks.js +++ b/src/core/js/ext_bookmarks.js @@ -2,18 +2,19 @@ * ** header v3.0 * This file is a part of the CaosDB Project. * - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020,2021 IndiScale GmbH <info@indiscale.com> * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@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 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. + * 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/>. @@ -231,7 +232,7 @@ var ext_bookmarks = function ($, logger, config) { */ const get_export_table = async function (bookmarks, preamble, tab, newline, leading_comments) { // TODO merge with related code in the module "caosdb_table_export". - preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8,": preamble); + preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8," : preamble); tab = tab || "%09"; newline = newline || "%0A"; leading_comments = (leading_comments ? leading_comments.join(newline) + newline : ""); @@ -489,10 +490,8 @@ var ext_bookmarks = function ($, logger, config) { counter = 0; update_collection([]); - // reset all buttons - get_bookmark_buttons().forEach((x) => { - set_button_state(x, false); - }); + // re-init to reset all buttons + init(); const storage_key_prefix = get_collection_prefix() remove_from_storage_by_prefix(storage_key_prefix); @@ -566,6 +565,78 @@ var ext_bookmarks = function ($, logger, config) { collection_id = id; } + /** + * Add a button to add all query results to bookmarks. + */ + const add_add_query_results_button = function () { + const row_id = "caosdb-f-add-query-to-bookmarks-row" + // do nothing if already existing + if ($("#" + row_id).length > 0) { + return; + } + + // do nothing if no results + if ($(".caosdb-query-response-results").text().trim() == "0") { + return; + } + + const button_html = $(`<div class="text-end" id=${row_id}> + <button class="btn btn-link" onclick="ext_bookmarks.add_query_results_to_bookmarks();">Bookmark all query results</button> +</div>`)[0]; + + // Add to query results box + $(".caosdb-query-response-heading").append(button_html); + } + + /** + * Execute select query and add all new ids to bookmarks. + */ + const add_query_results_to_bookmarks = async function () { + + const query_string = get_query_from_response(); + const waiting_notification = createWaitingNotification( + "Adding results to bookmarks. Please wait and do not reload the page."); + const bookmarks_row = $("#caosdb-f-add-query-to-bookmarks-row"); + bookmarks_row.find("button").hide(); + bookmarks_row.append(waiting_notification); + const resp = await query(query_string); + for (const eid of resp) { + bookmark_storage.setItem(get_key(getEntityID(eid)), getEntityID(eid)); + } + // re-init for correct display of counter and entities on page + init(); + removeAllWaitingNotifications(bookmarks_row); + bookmarks_row.find("button").prop("disabled", true).show(); + } + + /** + * Transform a given query it to a "SELECT ID FROM ..." query. + * + * @param {string} query_string + */ + const get_select_id_query_string = function (query_string) { + const test_string = query_string.toLowerCase(); + const select_string = "SELECT ID FROM "; + + // Will only be called on valid query results, so don't have to check + // for invalid query strings. + if (test_string.startsWith("find") || test_string.startsWith("count")) { + return select_string + query_string.slice(query_string.indexOf(" ") + 1); + } + if (test_string.startsWith("select")) { + return select_string + query_string.slice(test_string.indexOf("from ") + 5); + } + } + + /** + * Return the SELECT query created from the contents of the query response field + */ + const get_query_from_response = function () { + + const orig_query = $(".caosdb-f-query-response-string")[0].innerText; + return get_select_id_query_string(orig_query.trim()); + } + /** * Initialize this module. */ @@ -591,6 +662,10 @@ var ext_bookmarks = function ($, logger, config) { init_bookmark_buttons(e.target); }, true); } + + if ("${BUILD_MODULE_EXT_ADD_QUERY_TO_BOOKMARKS}" == "ENABLED") { + add_add_query_results_button(); + } } /** @@ -625,7 +700,6 @@ var ext_bookmarks = function ($, logger, config) { if (data_getters[data_key]) { uncached = (await data_getters[data_key](id)) } - // don't cache if getting the information is trivial or there are other // reasons why this is in the data_no_cache array. if (data_no_cache.indexOf(data_key) == -1) { @@ -652,6 +726,9 @@ var ext_bookmarks = function ($, logger, config) { get_bookmark_buttons: get_bookmark_buttons, init_button: init_button, get_bookmark_data: get_bookmark_data, + get_select_id_query_string: get_select_id_query_string, + get_query_from_response: get_query_from_response, + add_query_results_to_bookmarks: add_query_results_to_bookmarks, } }; @@ -666,10 +743,10 @@ $(document).ready(function () { // from the server. const get_path = async function (id) { if (id.indexOf("@") > -1) { - const entity = $(`[data-bmval='${id}']`); - if (entity.length > 0) { - return getEntityPath(entity[0]) || ""; - } + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getEntityPath(entity[0]) || ""; + } } return $(await transaction.retrieveEntityById(id)).attr("path"); } @@ -681,20 +758,20 @@ $(document).ready(function () { const get_name = async function (id) { if (id.indexOf("@") > -1) { - const entity = $(`[data-bmval='${id}']`); - if (entity.length > 0) { - return getEntityName(entity[0]) || ""; - } + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getEntityName(entity[0]) || ""; + } } return $(await transaction.retrieveEntityById(id)).attr("name"); } const get_rt = async function (id) { if (id.indexOf("@") > -1) { - const entity = $(`[data-bmval='${id}']`); - if (entity.length > 0) { - return getParents(entity[0]).join("/"); - } + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getParents(entity[0]).join("/"); + } } const parent_names = $(await transaction.retrieveEntityById(id)) .find("Parent").toArray().map(x => x.getAttribute("name")) @@ -727,4 +804,4 @@ $(document).ready(function () { ext_bookmarks = ext_bookmarks($, log.getLogger("ext_bookmarks"), config); caosdb_modules.register(ext_bookmarks); } -}); +}); \ No newline at end of file diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index dbb4e269a247cbbc40f1ea58623ec7b515dc2d57..efa28c9b39921df3e02a25ec95d4601f752fa288 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -164,19 +164,19 @@ this.navbar = new function () { // show form and hide the show_button const _in = () => { - // xs means viewport <= 768px - form.removeClass("d-none"); - form.addClass("d-xs-inline-block"); - show_button.removeClass("d-inline-block"); - show_button.addClass("d-none"); + // xs means viewport <= 768px + form.removeClass("d-none"); + form.addClass("d-xs-inline-block"); + show_button.removeClass("d-inline-block"); + show_button.addClass("d-none"); } // hide form and show the show_button const _out = () => { - // xs means viewport <= 768px - form.removeClass("d-xs-inline-block"); - form.addClass("d-none"); - show_button.removeClass("d-none"); - show_button.addClass("d-inline-block"); + // xs means viewport <= 768px + form.removeClass("d-xs-inline-block"); + form.addClass("d-none"); + show_button.removeClass("d-none"); + show_button.addClass("d-inline-block"); } show_button.on("click", () => { // show form... @@ -607,7 +607,7 @@ this.transformation = new function () { * @return {XMLDocument} xslt script */ this.retrieveEntityXsl = async function _rEX(root_template) { - const _root = root_template || '<xsl:template match="/" xmlns="http://www.w3.org/1999/xhtml"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>'; + const _root = root_template || '<xsl:template match="/" xmlns="http://www.w3.org/1999/xhtml"><div class="root"><xsl:apply-templates select="Response/child::*" mode="entities"/></div></xsl:template>'; var entityXsl = await transformation.retrieveXsltScript("entity.xsl"); var commonXsl = await transformation.retrieveXsltScript("common.xsl"); var errorXsl = await transformation.retrieveXsltScript('messages.xsl'); @@ -1313,7 +1313,7 @@ var queryForm = new function () { var submithandler = function () { // store current query var queryField = form.query; - var value = queryField.value.toUpperCase(); + var value = queryField.value.toUpperCase().trim(); if (typeof value == "undefined" || value.length == 0) { return; } @@ -1327,7 +1327,7 @@ var queryForm = new function () { paging = form.P.value } - queryForm.redirect(queryField.value, paging); + queryForm.redirect(queryField.value.trim(), paging); }; $("#caosdb-query-textarea").on("keydown", (e) => { @@ -1507,10 +1507,12 @@ function createErrorNotification(msg) { * Create a waiting notification with a informative message for the waiting user. * * @param {String} info, a message for the user + * @param {String} id, optional, the id of the message div. Default is empty * @return {HTMLElement} A div with class `caosdb-preview-waiting-notification`. */ -function createWaitingNotification(info) { - return $('<div class="' + globalClassNames.WaitingNotification + '">' + info + '</div>')[0]; +function createWaitingNotification(info, id) { + id = id ? `id="${id}"` : ""; + return $(`<div class="${globalClassNames.WaitingNotification}" ${id}>${info}</div>`)[0]; } /** @@ -1520,7 +1522,7 @@ function createWaitingNotification(info) { * @return {HTMLElement} The parameter `elem`. */ function removeAllWaitingNotifications(elem) { - $(elem.getElementsByClassName(globalClassNames.WaitingNotification)).remove(); + $(elem).find(`.${globalClassNames.WaitingNotification}`).remove(); return elem; } @@ -1925,4 +1927,4 @@ class _CaosDBModules { var caosdb_modules = new _CaosDBModules() -$(document).ready(initOnDocumentReady); +$(document).ready(initOnDocumentReady); \ No newline at end of file diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index d562bf99f7611b976e10d5349d0c99972d6d8fdd..92f08ea70645a8282f97f364c9fc4143f37afd6a 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -25,6 +25,10 @@ <xsl:output method="html"/> <!-- These little colored Rs, RTs, Ps, and Fs which hilite the beginning of a new entity. --> + <xsl:template match="Entity" mode="entity-heading-label"> + <span class="badge caosdb-f-entity-role caosdb-label-entity me-1" + title="This is an entity. The role is not specified.">E</span> + </xsl:template> <xsl:template match="Property" mode="entity-heading-label"> <span class="badge caosdb-f-entity-role caosdb-label-property me-1" data-entity-role="Property" title="This entity is a Property.">P</span> @@ -98,7 +102,7 @@ </div> </xsl:template> <!-- Main entry for ENTITIES --> - <xsl:template match="Property|Record|RecordType|File" mode="entities"> + <xsl:template match="Property|Record|RecordType|File|Response/Entity" mode="entities"> <div class="card caosdb-entity-panel mb-2"> <xsl:apply-templates select="Version" mode="entity-version-marker"/> <xsl:attribute name="id"> diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl index 6a95e656402bc8fcf1ac5789f2b213f09fdc8621..702a390f28ada96e140f40f10218b1740ab10700 100644 --- a/src/core/xsl/query.xsl +++ b/src/core/xsl/query.xsl @@ -56,7 +56,9 @@ <div class="col-sm-10 caosdb-overflow-box"> <div class="caosdb-overflow-content"> <span>Query: </span> - <xsl:value-of select="@string"/> + <span class="caosdb-f-query-response-string"> + <xsl:value-of select="@string"/> + </span> </div> </div> <div class="col-sm-2 text-end"> diff --git a/test/core/js/modules/ext_bookmarks.js.js b/test/core/js/modules/ext_bookmarks.js.js index 831df74231e479d5b4550524f6bc0da617c98fb3..8130fe422a5bd124ef1c6767708b036e2e904f53 100644 --- a/test/core/js/modules/ext_bookmarks.js.js +++ b/test/core/js/modules/ext_bookmarks.js.js @@ -42,16 +42,18 @@ QUnit.module("ext_bookmarks.js", { } }); -QUnit.test("parse_uri", function(assert) { +QUnit.test("parse_uri", function (assert) { assert.equal(typeof ext_bookmarks.parse_uri(""), "undefined"); assert.equal(typeof ext_bookmarks.parse_uri("asdf"), "undefined"); assert.equal(typeof ext_bookmarks.parse_uri("https://localhost:1234/Entity/sada?sadfasd#sdfgdsf"), "undefined"); - assert.propEqual(ext_bookmarks.parse_uri("safgsa/123&456&789?sadfasdf#_bm_1"), - {bookmarks: ["123", "456", "789"], collection_id: "1"}); + assert.propEqual(ext_bookmarks.parse_uri("safgsa/123&456&789?sadfasdf#_bm_1"), { + bookmarks: ["123", "456", "789"], + collection_id: "1" + }); }); -QUnit.test("get_bookmarks, clear_bookmark_storage", function(assert) { +QUnit.test("get_bookmarks, clear_bookmark_storage", function (assert) { assert.propEqual(ext_bookmarks.get_bookmarks(), []); ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("sdfg")] = "3456" @@ -67,9 +69,9 @@ QUnit.test("get_export_table", async function (assert) { const NEWL = "%0A"; const context_root = connection.getBasePath() + "Entity/"; var table = await ext_bookmarks.get_export_table( - ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]); + ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]); assert.equal(table, - `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${TAB}Name${TAB}RecordType${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${TAB}${TAB}${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${TAB}${TAB}${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${TAB}${TAB}${NEWL}101112${TAB}abcHead${TAB}${context_root}101112@abcHead${TAB}testpath_101112${TAB}${TAB}${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415${TAB}${TAB}`); + `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${TAB}Name${TAB}RecordType${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${TAB}${TAB}${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${TAB}${TAB}${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${TAB}${TAB}${NEWL}101112${TAB}abcHead${TAB}${context_root}101112@abcHead${TAB}testpath_101112${TAB}${TAB}${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415${TAB}${TAB}`); }); @@ -115,7 +117,7 @@ QUnit.test("update_export_link", function (assert) { QUnit.test("update_collection_link", function (assert) { const collection_link = $( - `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`); + `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`); const a = collection_link.find("a")[0]; $("body").append(collection_link); @@ -148,7 +150,8 @@ QUnit.test("bookmark buttons", function (assert) { const non_button = $(`<div data-bla="sadf"/>)`); const outside_button = $(`<div data-bmval="id3"/>`); const inside_buttons = $("<div/>").append([inactive_button, active_button, - broken_button, non_button]); + broken_button, non_button + ]); // get_bookmark_buttons assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 0); @@ -174,8 +177,51 @@ QUnit.test("bookmark buttons", function (assert) { assert.ok(inactive_button.is(".active")); ext_bookmarks.clear_bookmark_storage(); - assert.notOk(inactive_button.is(".active"), "clear_bookmark_storage removes active class"); inside_buttons.remove(); outside_button.remove(); }); + +QUnit.test("select-query transformation", function (assert) { + assert.equal( + ext_bookmarks.get_select_id_query_string("FIND analysis"), + "SELECT ID FROM analysis"); + assert.equal( + ext_bookmarks.get_select_id_query_string( + "FIND RECORD analysis WHICH HAS A date > 2012"), + "SELECT ID FROM RECORD analysis WHICH HAS A date > 2012"); + assert.equal( + ext_bookmarks.get_select_id_query_string( + "SELECT name, date FROM analysis"), + "SELECT ID FROM analysis"); + assert.equal( + ext_bookmarks.get_select_id_query_string("COUNT analysis"), + "SELECT ID FROM analysis"); + assert.equal( + ext_bookmarks.get_select_id_query_string("fInD analysis"), + "SELECT ID FROM analysis"); +}); + +QUnit.test("select-query extraction", function (assert) { + // Use response field copied from demo + const response_field = $(`<div class="card caosdb-query-response mb-2"> + <div class="card-header caosdb-query-response-heading"> + <div class="row"> + <div class="col-sm-10 caosdb-overflow-box"> + <div class="caosdb-overflow-content"> + <span>Query: </span> + <span class = "caosdb-f-query-response-string">SELECT name, id FROM RECORD MusicalAnalysis</span> + </div> + </div> + <div class="col-sm-2 text-end"> + <span>Results: </span> + <span class="caosdb-query-response-results">3</span> + </div> + </div> + </div> +</div>`); + $("body").append(response_field); + + assert.equal(ext_bookmarks.get_query_from_response(), + "SELECT ID FROM RECORD MusicalAnalysis"); +}); \ No newline at end of file diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index ad26e8455fb7ca16a9e6e9687606b6aba1d0e7e6..d2ef27952e41142a62eb70e144571bc9d30c52d2 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -709,12 +709,6 @@ QUnit.test("removeAllErrorNotifications", function (assert) { '<div class="caosdb-preview-waiting-notification">Please wait!</div></div>')[0]; let emptyElem = $('<div></div>')[0]; - assert.throws(() => { - preview.removeAllErrorNotifications(); - }, "no parameter throws"); - assert.throws(() => { - preview.removeAllErrorNotifications(null); - }, "null parameter throws"); assert.equal(okElem.childNodes.length, 3, "before: three children"); assert.equal(okElem, preview.removeAllErrorNotifications(okElem), "return first parameter"); @@ -731,13 +725,6 @@ QUnit.test("removeAllWaitingNotifications", function (assert) { '<div class="caosdb-preview-error-notification">Error!</div></div>')[0]; let emptyElem = $('<div></div>')[0]; - assert.throws(() => { - removeAllWaitingNotifications(); - }, "no parameter throws"); - assert.throws(() => { - removeAllWaitingNotifications(null); - }, "null parameter throws"); - assert.equal(okElem.childNodes.length, 3, "before: three children"); assert.equal(okElem, removeAllWaitingNotifications(okElem), "return first parameter"); assert.equal(okElem.childNodes.length, 1, "after: one child");