From 2b75f83740b5c40a553e9982bbf7a9112b4f5bfa Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Wed, 28 May 2025 08:05:09 +0200 Subject: [PATCH 1/5] Seems to work, yet to add cypress tests --- .../CustomQueryEditor/customEditor.jsx | 3 ++- .../QueryResultList/QueryResultList.jsx | 8 ++++--- .../TemplatedListResultTable.jsx | 23 +++++++++++++++---- main/src/dataProvider/SparqlDataProvider.js | 2 +- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/main/src/components/CustomQueryEditor/customEditor.jsx b/main/src/components/CustomQueryEditor/customEditor.jsx index ad0ce0e..177fe98 100644 --- a/main/src/components/CustomQueryEditor/customEditor.jsx +++ b/main/src/components/CustomQueryEditor/customEditor.jsx @@ -243,7 +243,8 @@ export default function CustomEditor(props) { icon: customQuery.icon }); - navigate(`/${customQuery.id}`); + // force a re-render with the updateTimestamp + navigate(`/${customQuery.id}`, {/* TODO? replace: true,*/ state: { updateTimestamp: Date.now() } }); } }; diff --git a/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx b/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx index dd0f4d6..f8bda77 100644 --- a/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx +++ b/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx @@ -1,4 +1,4 @@ -import { Component } from "react"; +import { Component, useEffect } from "react"; import { Datagrid, List, Title, Loading, useListContext, useResourceDefinition } from "react-admin"; import ActionBar from "../../ActionBar/ActionBar"; import GenericField from "../../../representationProvider/GenericField"; @@ -21,7 +21,7 @@ import CustomConversionButton from "../../CustomQueryEditor/customConversionButt * @returns {Component} custom ListViewer as defined by react-admin containing the results of the query with each variable its generic field. */ function QueryResultList(props) { - const { resource, variableValues, changeVariables, submitted } = props; + const { updateTimestamp, resource, variableValues, changeVariables, submitted } = props; const resourceDef = useResourceDefinition(); const queryTitle = resourceDef?.options?.label; const config = configManager.getConfig(); @@ -54,7 +54,8 @@ function QueryResultList(props) { empty={false} queryOptions={{ meta: { - variableValues: variableValues + variableValues, + updateTimestamp // force the dataProvider to refetch the data when the updateTimestamp changes }}}> @@ -63,6 +64,7 @@ function QueryResultList(props) { } QueryResultList.propTypes = { + updateTimestamp: PropTypes.number.isRequired, resource: PropTypes.string.isRequired, variableValues: PropTypes.object.isRequired, changeVariables: PropTypes.func.isRequired, diff --git a/main/src/components/ListResultTable/TemplatedListResultTable.jsx b/main/src/components/ListResultTable/TemplatedListResultTable.jsx index 53245c6..beae5b8 100644 --- a/main/src/components/ListResultTable/TemplatedListResultTable.jsx +++ b/main/src/components/ListResultTable/TemplatedListResultTable.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, Component } from 'react'; +import { useState, useEffect, Component } from 'react'; import { useResourceContext, Loading, useDataProvider, useResourceDefinition } from "react-admin"; import { useLocation, useNavigate } from 'react-router-dom'; import TemplatedQueryForm from "./TemplatedQueryForm.jsx"; @@ -21,6 +21,7 @@ const TemplatedListResultTable = (props) => { const location = useLocation(); const navigate = useNavigate(); const query = configManager.getQueryWorkingCopyById(resource); + const [updateTimestamp, setUpdateTimestamp] = useState(0); const [askingForVariableOptions, setAskingForVariableOptions] = useState(false); const [waitingForVariableOptions, setWaitingForVariableOptions] = useState(false); const [variableOptionsError, setVariableOptionsError] = useState(""); @@ -33,6 +34,7 @@ const TemplatedListResultTable = (props) => { // LOG console.log(`--- TemplatedListResultTable #${++templatedListResultTableCounter}`); // LOG console.log(`props: ${JSON.stringify(props, null, 2)}`); // LOG console.log(`resource: ${resource}`); + // LOG console.log(`updateTimestamp: ${updateTimestamp}`); // LOG console.log(`askingForVariableOptions: ${askingForVariableOptions}`); // LOG console.log(`waitingForVariableOptions: ${waitingForVariableOptions}`); // LOG console.log(`variableOptionsError: ${variableOptionsError}`); @@ -42,6 +44,20 @@ const TemplatedListResultTable = (props) => { // LOG console.log(`isTemplatedQuery: ${isTemplatedQuery}`); // LOG console.log(`templatedQueryFormEnabled: ${templatedQueryFormEnabled}`); + useEffect(() => { + const t = location.state?.updateTimestamp; + if (t && t != updateTimestamp) { + setUpdateTimestamp(location.state.updateTimestamp); + // LOG console.log(`New updateTimestamp: ${t}`); + setAskingForVariableOptions(false); + setWaitingForVariableOptions(false); + setVariableOptionsError(""); + setVariableOptions({}); + setVariableValues({}); + setVariablesSubmitted(false); + } + }, [location.state]); + useEffect(() => { (async () => { if (askingForVariableOptions) { @@ -62,7 +78,6 @@ const TemplatedListResultTable = (props) => { })(); }, [askingForVariableOptions]); - // Cover a transient state after creation of a new custom query. EventEmitter's event processing may still be in progress. if (!resourceDef.options) { // LOG console.log('TemplatedListResultTable waiting for custom query creation to complete.'); @@ -130,8 +145,8 @@ const TemplatedListResultTable = (props) => { return ( templatedQueryFormEnabled - ? - : + ? + : ) } diff --git a/main/src/dataProvider/SparqlDataProvider.js b/main/src/dataProvider/SparqlDataProvider.js index e091402..1b6e70a 100644 --- a/main/src/dataProvider/SparqlDataProvider.js +++ b/main/src/dataProvider/SparqlDataProvider.js @@ -51,7 +51,7 @@ export default { query.variableValues = meta.variableValues; } - const hash = JSON.stringify({ resource, sort, variableValues: query.variableValues }); + const hash = JSON.stringify({ resource, sort, meta }); // LOG console.log(`hash: ${hash}`); if (hash == listCache.hash) { // LOG console.log(`reusing listCache.results: ${JSON.stringify(listCache.results, null, 2)}`); From b9e90cd13cff79509af31d9012d35620f62b8bbd Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Wed, 28 May 2025 17:11:18 +0200 Subject: [PATCH 2/5] update with extended cypress tests and final corrections --- .../CustomQueryEditor/customEditor.jsx | 2 +- .../TemplatedListResultTable.jsx | 3 + main/src/dataProvider/SparqlDataProvider.js | 15 +- test/cypress/e2e/custom-query-editor.cy.js | 150 +++++++++++------- 4 files changed, 107 insertions(+), 63 deletions(-) diff --git a/main/src/components/CustomQueryEditor/customEditor.jsx b/main/src/components/CustomQueryEditor/customEditor.jsx index 177fe98..6e94b15 100644 --- a/main/src/components/CustomQueryEditor/customEditor.jsx +++ b/main/src/components/CustomQueryEditor/customEditor.jsx @@ -244,7 +244,7 @@ export default function CustomEditor(props) { }); // force a re-render with the updateTimestamp - navigate(`/${customQuery.id}`, {/* TODO? replace: true,*/ state: { updateTimestamp: Date.now() } }); + navigate(`/${customQuery.id}`, {state: { updateTimestamp: Date.now() } }); } }; diff --git a/main/src/components/ListResultTable/TemplatedListResultTable.jsx b/main/src/components/ListResultTable/TemplatedListResultTable.jsx index beae5b8..ef32f93 100644 --- a/main/src/components/ListResultTable/TemplatedListResultTable.jsx +++ b/main/src/components/ListResultTable/TemplatedListResultTable.jsx @@ -6,6 +6,7 @@ import QueryResultList from "./QueryResultList/QueryResultList"; import ErrorDisplay from "../../components/ErrorDisplay/ErrorDisplay"; import configManager from '../../configManager/configManager.js'; +import comunicaEngineWrapper from '../../comunicaEngineWrapper/comunicaEngineWrapper.js'; // LOG let templatedListResultTableCounter = 0; @@ -55,6 +56,8 @@ const TemplatedListResultTable = (props) => { setVariableOptions({}); setVariableValues({}); setVariablesSubmitted(false); + // we need next because comunica would use its cache even if some of its context parameters have changed + comunicaEngineWrapper.reset(); } }, [location.state]); diff --git a/main/src/dataProvider/SparqlDataProvider.js b/main/src/dataProvider/SparqlDataProvider.js index 1b6e70a..5af5329 100644 --- a/main/src/dataProvider/SparqlDataProvider.js +++ b/main/src/dataProvider/SparqlDataProvider.js @@ -51,14 +51,13 @@ export default { query.variableValues = meta.variableValues; } - const hash = JSON.stringify({ resource, sort, meta }); + const hash = JSON.stringify({ resource, sort, meta, query }); // LOG console.log(`hash: ${hash}`); if (hash == listCache.hash) { // LOG console.log(`reusing listCache.results: ${JSON.stringify(listCache.results, null, 2)}`); results = listCache.results; } else { if (query.comunicaContext?.sources?.length) { - // LOG console.log(`query.queryText: ${ query.queryText }`); results = await executeQuery(query); listCache.hash = hash; listCache.results = results; @@ -327,17 +326,19 @@ async function getSourcesFromSourcesIndex(sourcesIndex, httpProxies) { function handleComunicaContextCreation(query) { if (!query.comunicaContext) { query.comunicaContext = { - sources: [], - lenient: true + sources: [] }; } else { - if (query.comunicaContext.lenient === undefined) { - query.comunicaContext.lenient = true; - } if (!query.comunicaContext.sources) { query.comunicaContext.sources = []; } } + + if (query.sourcesIndex) { + if (query.comunicaContext.lenient === undefined) { + query.comunicaContext.lenient = true; + } + } } async function getVariableOptions(query) { diff --git a/test/cypress/e2e/custom-query-editor.cy.js b/test/cypress/e2e/custom-query-editor.cy.js index d06f202..303d807 100644 --- a/test/cypress/e2e/custom-query-editor.cy.js +++ b/test/cypress/e2e/custom-query-editor.cy.js @@ -14,7 +14,17 @@ describe("Custom Query Editor tests", () => { cy.get('button[type="submit"]').click(); cy.contains("Invalid SPARQL query."); - cy.setCodeMirrorValue("#sparql-edit-field-queryString", `PREFIX schema: + // This incomplete SPARQL query passes the SPARQL edit field syntax checker, but will fail when executed + cy.setCodeMirrorValue("#sparql-edit-field-queryString", "SELECT") + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.contains("Something went wrong").should('exist'); + + cy.get('button').contains("Edit Query").click(); + + cy.setCodeMirrorValue("#sparql-edit-field-queryString", `PREFIX schema: SELECT * WHERE { ?list schema:name ?listTitle; @@ -31,6 +41,26 @@ describe("Custom Query Editor tests", () => { // Checking if the book query works cy.contains("Colleen Hoover").should('exist'); + + // Check if updating the custom query results in changed results - here we just change a column name + cy.get('button').contains("Edit Query").click(); + + cy.setCodeMirrorValue("#sparql-edit-field-queryString", `PREFIX schema: + + SELECT * WHERE { + ?list schema:name ?listTitle; + schema:itemListElement [ + schema:name ?bookTitleColumnNameChangedXXX; + schema:creator [ + schema:name ?authorName + ] + ]. + }`); + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.contains("bookTitleColumnNameChangedXXX").should('exist'); }); it("Create a new simple query with a Comunica context", () => { @@ -71,6 +101,16 @@ describe("Custom Query Editor tests", () => { // Checking if the book query works cy.contains("Colleen Hoover").should('exist'); + + // Check if updating the custom query results in changed results - here we just undo the Comunica context, resulting in Comunica failing to fetch + cy.get('button').contains("Edit Query").click(); + + cy.get('input[name="comunicaContextCheck"]').click(); + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.contains("Something went wrong").should('exist'); }); it("Create a new query, with multiple sources", () => { @@ -99,7 +139,7 @@ WHERE { } ORDER BY ?componentName `); - + cy.get('input[name="source"]').type("http://localhost:8080/verifiable-example/components-vc ; http://localhost:8080/verifiable-example/components-vc-incorrect-proof ; http://localhost:8080/example/components"); cy.get('[data-cy="parsingError"]').should('not.exist'); @@ -107,6 +147,16 @@ ORDER BY ?componentName // Checking if the query works cy.contains("https://www.example.com/data/component-c01").should('exist'); + + // Check if updating the custom query results in changed results - here we just add something to the last source, making it a not existing source, resulting in Comunica failing to fetch + cy.get('button').contains("Edit Query").click(); + + cy.get('input[name="source"]').type("hihihahahoho"); + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.contains("Something went wrong").should('exist'); }); it("Create a new query, here an ASK query", () => { @@ -123,7 +173,7 @@ ASK WHERE { ?person foaf:name ?name. ?person dbo:influencedBy dbp:Pablo_Picasso. }`); - + cy.get('input[name="source"]').type("http://localhost:8080/example/artists"); cy.get('input[name="askQueryCheck"]').click() @@ -144,6 +194,16 @@ ASK WHERE { // Check if the query works cy.contains("Yes, there is at least one artist influenced by Picasso!") + + // Check if updating the custom query results in changed results - here we just change the askQuery details + cy.get('button').contains("Edit Query").click(); + + cy.setCodeMirrorValue("#json-edit-field-askQuery", '{"trueText":"Yezzzzz","falseText":"Noooooooooo"}') + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.contains("Yezzzzz") }); it("Create a new query, here with http proxies", () => { @@ -160,7 +220,7 @@ SELECT ?name ?birthDate_int WHERE { schema:birthDate ?birthDate_int; ]. }`); - + cy.get('input[name="source"]').type("http://localhost:8001/example/idols"); cy.get('input[name="httpProxiesCheck"]').click() @@ -181,6 +241,16 @@ SELECT ?name ?birthDate_int WHERE { // Check if the query works cy.contains("1-2 of 2"); + + // Check if updating the custom query results in changed results - here we just change the httpProxies details to point to a not existing proxy, resulting in Comunica failing to fetch + cy.get('button').contains("Edit Query").click(); + + cy.setCodeMirrorValue("#json-edit-field-httpProxies", '[{"urlStart":"http://localhost:8001","httpProxy":"http://localhost:9999/"}, {"urlStart":"http://localhost:8002","httpProxy":"http://localhost:9000/"}]'); + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.contains("Something went wrong").should('exist'); }); it("Check if all possible parameters are filled in with parameterized URL", () => { @@ -209,57 +279,6 @@ SELECT ?name ?birthDate_int WHERE { cy.contains("Invalid SPARQL query."); }); - it("Successfully edit a query to make it work", () => { - cy.visit("/#/customQuery"); - - // First create a wrong query - cy.get('input[name="name"]').type("broken query"); - cy.get('textarea[name="description"]').type("just a description"); - - // This incomplete SPARQL query passes the SPARQL edit field syntax checker, but will fail when executed - cy.setCodeMirrorValue("#sparql-edit-field-queryString", "SELECT") - - cy.get('input[name="source"]').type("http://localhost:8080/example/wish-list"); - - // Submit the incomplete query - cy.get('[data-cy="parsingError"]').should('not.exist'); - cy.get('button[type="submit"]').click(); - - cy.contains("Custom queries").click(); - cy.contains("broken query").click(); - - // Verify that the faulty query results in an error message - cy.contains("Something went wrong").should('exist'); - - // Edit the query - cy.get('button').contains("Edit Query").click(); - - // Give the query a new name and a correct query text - cy.get('input[name="name"]').clear(); - cy.get('input[name="name"]').type("Fixed query"); - - cy.setCodeMirrorValue("#sparql-edit-field-queryString", `PREFIX schema: -SELECT * WHERE { - ?list schema:name ?listTitle; - schema:itemListElement [ - schema:name ?bookTitle; - schema:creator [ - schema:name ?authorName - ] - ]. -}`); - - // Submit the correct query - cy.get('[data-cy="parsingError"]').should('not.exist'); - cy.get('button[type="submit"]').click(); - - // Now we should be on the page of the fixed query - cy.contains("Fixed query").should('exist'); - - // Check if the resulting list appears - cy.contains("Colleen Hoover").should('exist'); - }); - it("Shares the correct URL", () => { cy.visit("/#/customQuery"); @@ -402,6 +421,17 @@ WHERE { cy.get('button[type="submit"]').click(); cy.contains("https://www.example.com/data/component-c01").should('exist'); + + // Check if updating the custom query results in changed results - here we just change the indexSourceUrl, resulting in Comunica failing to fetch + cy.get('button').contains("Edit Query").click(); + + cy.get('input[name="indexSourceUrl"]').clear(); + cy.get('input[name="indexSourceUrl"]').type("http://localhost:8080/example/huppledepup-does-not-exist") + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.contains("The result list is empty (no sources found).").should('exist'); }); it("Make a templated query, then edit it to make it a normal query", () => { @@ -569,6 +599,16 @@ schema:sameAs ?sameAs_url; cy.get('.column-name').find('span').contains("Franz Schubert").should("not.exist"); cy.get('.column-name').find('span').contains("Ludwig van Beethoven").should("not.exist"); + + // Check if updating the custom query results in changed results - here we just change the first indirectVariablesQuery + cy.get('button').contains("Edit Query").click(); + + cy.setCodeMirrorValue("#sparql-edit-field-indirectVariablesQuery-0", "PREFIX schema: SELECT DISTINCT ?geEEEnre WHERE { ?list schema:genre ?geEEEnre; }") + + cy.get('[data-cy="parsingError"]').should('not.exist'); + cy.get('button[type="submit"]').click(); + + cy.get('.ra-input-geEEEnre').should('exist'); }); it("Custom templated query with 2 indirect variables", () => { From 83a285837cf946b7bdd95b4fc28502c261969fe7 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Wed, 28 May 2025 17:41:55 +0200 Subject: [PATCH 3/5] Custom query editor layout tweak --- .../CustomQueryEditor/customEditor.jsx | 202 +++++++++--------- 1 file changed, 100 insertions(+), 102 deletions(-) diff --git a/main/src/components/CustomQueryEditor/customEditor.jsx b/main/src/components/CustomQueryEditor/customEditor.jsx index 6e94b15..15fda6f 100644 --- a/main/src/components/CustomQueryEditor/customEditor.jsx +++ b/main/src/components/CustomQueryEditor/customEditor.jsx @@ -354,6 +354,18 @@ export default function CustomEditor(props) { Comunica Context & Sources +
} label="Advanced Comunica Context Settings" /> -
- - - {isChecked(formData.comunicaContextCheck) &&
} - { - setFormData((prevFormData) => ({ - ...prevFormData, - 'sourceIndexCheck': !isChecked(formData.sourceIndexCheck), - })); +
+ { + setFormData((prevFormData) => ({ + ...prevFormData, + 'sourceIndexCheck': !isChecked(formData.sourceIndexCheck), + })); + } } - } - />} label="Indirect sources" /> + />} label="Indirect sources" /> +
{isChecked(formData.sourceIndexCheck) &&
@@ -456,21 +455,23 @@ export default function CustomEditor(props) { } } />} label="Fixed Variables" /> +
- {isChecked(formData.directVariablesCheck) && -
- Give the variable names and options for this templated query. - -
- } + {isChecked(formData.directVariablesCheck) && +
+ Give the variable names and options for this templated query. + +
+ } +
} label="Indirect Variables" /> +
- {isChecked(formData.indirectVariablesCheck) && -
-
- Give one or more SPARQL queries to retrieve variable(s) from source(s). -
- { - indirectVariablesQueryList.map((ivQuery, index) => ( -
- - - -
- )) - } - + {isChecked(formData.indirectVariablesCheck) && +
+
+ Give one or more SPARQL queries to retrieve variable(s) from source(s).
- } -
+ { + indirectVariablesQueryList.map((ivQuery, index) => ( +
+ + + +
+ )) + } + +
+ } Extra Options
- } label="ASK query" /> - - {isChecked(formData.askQueryCheck) && -
- -
- } - +
+ {isChecked(formData.askQueryCheck) && +
+ +
+ } +
} label="Http proxies" /> - - {isChecked(formData.httpProxiesCheck) && -
- -
- } -
+ {isChecked(formData.httpProxiesCheck) && +
+ +
+ }
From 42f655f67257866bbc3e3c48cc0479476723aba9 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Wed, 28 May 2025 17:49:22 +0200 Subject: [PATCH 4/5] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a72b2c3..159dd12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the resolved query is executed immediately, avoiding the delay it takes to first retrieve all options for the variables (#211). - CONSTRUCT queries work again (#222). - Config field "logoRedirectURL" is working (#17). +- After modifying an existing custom query, the previous result table is updated as expected now (#137). ## [1.7.0] - 2025-04-09 From 20878370b82e3d3c53e06cbda9027a9bfb5003d1 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Wed, 28 May 2025 18:13:13 +0200 Subject: [PATCH 5/5] review finishing touches --- .../ListResultTable/QueryResultList/QueryResultList.jsx | 2 +- .../components/ListResultTable/TemplatedListResultTable.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx b/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx index f8bda77..5052d7b 100644 --- a/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx +++ b/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx @@ -1,4 +1,4 @@ -import { Component, useEffect } from "react"; +import { Component } from "react"; import { Datagrid, List, Title, Loading, useListContext, useResourceDefinition } from "react-admin"; import ActionBar from "../../ActionBar/ActionBar"; import GenericField from "../../../representationProvider/GenericField"; diff --git a/main/src/components/ListResultTable/TemplatedListResultTable.jsx b/main/src/components/ListResultTable/TemplatedListResultTable.jsx index ef32f93..68dba01 100644 --- a/main/src/components/ListResultTable/TemplatedListResultTable.jsx +++ b/main/src/components/ListResultTable/TemplatedListResultTable.jsx @@ -148,8 +148,8 @@ const TemplatedListResultTable = (props) => { return ( templatedQueryFormEnabled - ? - : + ? + : ) }