diff --git a/CHANGELOG.md b/CHANGELOG.md index 663ba10..fb61bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Avoided (infinite) display of "The list is loading. Just a moment please." for queries that should show "The result list is empty.", in cases where the user does not have the right to read the involved source(s) (#209). - Result list sorting works again and the behavior is improved - see the issue for details (#216). - Loading values for variables ends with an error message if a required source is not available (#218). +- When visiting a templated query with variable values defined in the URL search parameters, + the resolved query is executed immediately, avoiding the delay it takes to first retrieve all options for the variables (#211). ## [1.7.0] - 2025-04-09 diff --git a/main/src/components/ListResultTable/TemplatedListResultTable.jsx b/main/src/components/ListResultTable/TemplatedListResultTable.jsx index c75706a..53245c6 100644 --- a/main/src/components/ListResultTable/TemplatedListResultTable.jsx +++ b/main/src/components/ListResultTable/TemplatedListResultTable.jsx @@ -21,8 +21,9 @@ const TemplatedListResultTable = (props) => { const location = useLocation(); const navigate = useNavigate(); const query = configManager.getQueryWorkingCopyById(resource); - const [waitingForVariableOptions, setWaitingForVariableOptions] = useState(!!(query.variables || query.indirectVariables)); - const [waitingForVariableOptionsError, setWaitingForVariableOptionsError] = useState(""); + const [askingForVariableOptions, setAskingForVariableOptions] = useState(false); + const [waitingForVariableOptions, setWaitingForVariableOptions] = useState(false); + const [variableOptionsError, setVariableOptionsError] = useState(""); const [variableOptions, setVariableOptions] = useState({}); const [variableValues, setVariableValues] = useState({}); const [variablesSubmitted, setVariablesSubmitted] = useState(false); @@ -32,8 +33,9 @@ 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(`askingForVariableOptions: ${askingForVariableOptions}`); // LOG console.log(`waitingForVariableOptions: ${waitingForVariableOptions}`); - // LOG console.log(`waitingForVariableOptionsError: ${waitingForVariableOptionsError}`); + // LOG console.log(`variableOptionsError: ${variableOptionsError}`); // LOG console.log(`variableOptions: ${JSON.stringify(variableOptions, null, 2)}`); // LOG console.log(`variableValues: ${JSON.stringify(variableValues, null, 2)}`); // LOG console.log(`variablesSubmitted: ${variablesSubmitted}`); @@ -42,21 +44,23 @@ const TemplatedListResultTable = (props) => { useEffect(() => { (async () => { - if (query.variables || query.indirectVariables) { + if (askingForVariableOptions) { + setAskingForVariableOptions(false); try { - // LOG console.log('start waiting for variable options'); + // LOG console.log('Start waiting for variable options'); + setWaitingForVariableOptions(true); // LOG const t1 = Date.now(); setVariableOptions(await dataProvider.getVariableOptions(query)); // LOG const t2 = Date.now(); - // LOG console.log(`done waiting for variable options after ${t2-t1} ms`); + // LOG console.log(`Done waiting for variable options after ${t2-t1} ms`); setWaitingForVariableOptions(false); } catch (error) { - // LOG console.log(`error getting variable options: ${error.message}`); - setWaitingForVariableOptionsError(error.message); + // LOG console.log(`Error getting variable options: ${error.message}`); + setVariableOptionsError(error.message); } } })(); - }, [resource]); + }, [askingForVariableOptions]); // Cover a transient state after creation of a new custom query. EventEmitter's event processing may still be in progress. @@ -65,31 +69,43 @@ const TemplatedListResultTable = (props) => { return false; } - if (waitingForVariableOptionsError) { - // LOG console.log(`TemplatedListResultTable failure while waiting for variable options: ${waitingForVariableOptionsError}`); - return ; - } - - if (waitingForVariableOptions) { - // LOG console.log('TemplatedListResultTable waiting for variable options.'); - return ; + if (variableOptionsError) { + // LOG console.log(`TemplatedListResultTable variable options error: ${variableOptionsError}`); + return ; } if (isTemplatedQuery) { - // Check if an update of variable values is needed from user supplied url search parameters - const possibleNewVariableValues = variableValuesFromUrlSearchParams(new URLSearchParams(location.search), variableOptions); - // Protect against incomplete or omitted variable values, as is the case when changing pagination, - // where List causes a revisit but does not include variable values in url search parameters - if (Object.keys(possibleNewVariableValues).length == Object.keys(variableOptions).length) { - if (Object.keys(variableOptions).some((v) => variableValues[v] != possibleNewVariableValues[v])) { - // LOG console.log("Accepting new variable values from user supplied url search parameters."); - setVariableValues(possibleNewVariableValues); + if (!Object.keys(variableOptions).length) { + // Check for initial visit with url search parameters + if (!askingForVariableOptions && !waitingForVariableOptions && !variablesSubmitted) { + const vv = initialVariableValuesFromUrlSearchParams(new URLSearchParams(location.search)); + if (Object.keys(vv).length) { + // LOG console.log("Accepting initial variable values from url search parameters."); + setVariableValues(vv); + setVariablesSubmitted(true); + return false; + } + // LOG console.log("Trigger the search for variable options in body."); + setAskingForVariableOptions(true); + return false; + } + } else { + // Check for next visit with url search parameters + const vv = newVariableValuesFromUrlSearchParams(new URLSearchParams(location.search), variableValues); + if (vv && Object.keys(variableValues).some((v) => variableValues[v] != vv[v])) { + // LOG console.log("Accepting new variable values from url search parameters."); + setVariableValues(vv); setVariablesSubmitted(true); return false; } } } + if (waitingForVariableOptions) { + // LOG console.log('TemplatedListResultTable waiting for variable options.'); + return ; + } + const submitVariables = (formVariables) => { // Create url search parameters from new variable values received from the TemplatedQueryForm fields // Note: possible previous url search parameters involving pagination are discarded here on purpose @@ -102,6 +118,12 @@ const TemplatedListResultTable = (props) => { const changeVariables = () => { setVariablesSubmitted(false); + if (!Object.keys(variableOptions).length) { + if (!askingForVariableOptions) { + // LOG console.log("Trigger the search for variable options in changeVariables."); + setAskingForVariableOptions(true); + } + } // revisit with same search parameters navigate(location.search); } @@ -129,19 +151,35 @@ function urlSearchParamsFromVariableValues(variableValues) { } /** - * Make variableValues from urlSearchParams + * Make initial variableValues from urlSearchParams * @param {URLSearchParams} urlSearchParams - * @param {Object} variableOptions used to filter * @returns {Object} variableValues */ -function variableValuesFromUrlSearchParams(urlSearchParams, variableOptions) { +function initialVariableValuesFromUrlSearchParams(urlSearchParams) { const variableValues = {}; - for (const variableName of Object.keys(variableOptions)) { + for (const [key, value] of urlSearchParams.entries()) { + variableValues[key] = value; + } + return variableValues; +} + +/** + * Make new variableValues from urlSearchParams + * @param {URLSearchParams} urlSearchParams the input + * @param {Object} variableValues the current variableValues + * @returns {Object} new variableValues or undefined, if not all variable keys in the input + */ +function newVariableValuesFromUrlSearchParams(urlSearchParams, variableValues) { + const newVariableValues = {}; + for (const variableName of Object.keys(variableValues)) { if (urlSearchParams.has(variableName)) { - variableValues[variableName] = urlSearchParams.get(variableName); + newVariableValues[variableName] = urlSearchParams.get(variableName); } } - return variableValues; + if (Object.keys(newVariableValues).length == Object.keys(variableValues).length) { + return newVariableValues; + } + return undefined; } export default TemplatedListResultTable; diff --git a/test/cypress/e2e/indirect-variables.cy.js b/test/cypress/e2e/indirect-variables.cy.js deleted file mode 100644 index 3deeef2..0000000 --- a/test/cypress/e2e/indirect-variables.cy.js +++ /dev/null @@ -1,120 +0,0 @@ -describe("Indirect variable query", () => { - - it("Indirect with 1 variable", () => { - - cy.visit("/"); - cy.contains("Example queries").click(); - cy.contains("A templated query about musicians (indirect variables)").click(); - - // Fill in the form - cy.get('.ra-input-genre').click(); - cy.get('li').contains('Baroque').click(); - - // Comfirm query - cy.get('button').contains('Query').click(); - - // Check that the page loaded and that we can see the correct data - cy.contains("Finished in:"); - cy.get('.column-name').find('span').contains("Johann Sebastian Bach").should("exist");; - cy.get('.column-name').find('span').contains("Marc-Antoine Charpentier").should("exist");; - // Check that we don't see artists that don't belong here - cy.get('.column-name').find('span').contains("Franz Schubert").should("not.exist"); - cy.get('.column-name').find('span').contains("Wolfgang Amadeus Mozart").should("not.exist"); - - }); - - it("Indirect with 2 variables", () => { - - cy.visit("/"); - cy.contains("For testing only").click(); - cy.contains("A templated query about musicians, two variables (indirect variables)").click(); - - // Fill in the form - cy.get('.ra-input-genre').click(); - cy.get('li').contains('Classical').click(); - - cy.get('.ra-input-sameAsUrl').click(); - cy.get('li').contains('Mozart').click(); - - cy.get('button').contains('Query').click(); - - // Check that it is correctly loaded with and only the correct data appears - cy.contains("Finished in:"); - cy.get('.column-name').find('span').contains("Wolfgang Amadeus Mozart").should("exist");; - - cy.get('.column-name').find('span').contains("Franz Schubert").should("not.exist"); - cy.get('.column-name').find('span').contains("Johann Sebastian Bach").should("not.exist"); - cy.get('.column-name').find('span').contains("Ludwig van Beethoven").should("not.exist"); - - }); - - - it("Indirect with 1 variable and sources from indexfile", () => { - - cy.visit("/"); - cy.contains("For testing only").click(); - cy.contains("Component and materials - 1 variable (indirect source & indirect variables)").click(); - - // Fill in the form - cy.get('.ra-input-componentName').click(); - cy.get('li').contains('Component 1').click(); - - // Comfirm query - cy.get('button[type="submit"]').click(); - - // Check that it is correctly loaded with and only the correct data appears - cy.contains("Finished in:"); - - cy.get('.column-componentName').find('span').contains("Component 1").should("exist"); - cy.get('.column-materialName').find('span').contains("Material 2").should("exist"); - cy.get('.column-materialName').find('span').contains("Material 1").should("exist"); - - cy.get('.column-componentName').find('span').contains("Component 2").should("not.exist"); - cy.get('.column-componentName').find('span').contains("Component 3").should("not.exist"); - cy.get('.column-materialName').find('span').contains("Material 6").should("not.exist"); - - - - }); - - it("Indirect with 2 variables and sources from indexfile", () => { - cy.visit("/"); - cy.contains("For testing only").click(); - cy.contains("Component and materials - 2 variables (indirect source & indirect variables)").click(); - - // Fill in the form - cy.get('.ra-input-componentName').click(); - cy.get('li').contains('Component 1').click(); - - cy.get('.ra-input-materialName').click(); - cy.get('li').contains('Material 2').click(); - - // Comfirm query - cy.get('button[type="submit"]').click(); - - // Check that it is correctly loaded with and only the correct data appears - cy.contains("Finished in:"); - - cy.get('.column-componentName').find('span').contains("Component 1").should("exist"); - cy.get('.column-materialName').find('span').contains("Material 2").should("exist"); - - cy.get('.column-materialName').find('span').contains("Material 1").should("not.exist"); - cy.get('.column-componentName').find('span').contains("Component 2").should("not.exist"); - cy.get('.column-componentName').find('span').contains("Component 3").should("not.exist"); - cy.get('.column-materialName').find('span').contains("Material 6").should("not.exist"); - - - }); - - it("Indirect with 1 variable but no sources available to get variables from", () => { - - cy.visit("/"); - cy.contains("For testing only").click(); - cy.contains("A templated query about musicians (indirect variables) - no sources").click(); - - cy.contains("Error getting variable options...").should("exist");; - }); -}); - - - diff --git a/test/cypress/e2e/templated-query.cy.js b/test/cypress/e2e/templated-query.cy.js index 2e1faa0..b7046b2 100644 --- a/test/cypress/e2e/templated-query.cy.js +++ b/test/cypress/e2e/templated-query.cy.js @@ -141,61 +141,6 @@ describe("Templated query", () => { }); - it("With 2 variables; visit with variable values given in url search parameters", () => { - // a first url - cy.visit("/#/1100?genre=%22Classical%22&sameAsUrl=%3Chttps%3A%2F%2Fen.wikipedia.org%2Fwiki%2FWolfgang_Amadeus_Mozart%3E"); - - // Check the display of the variable(s) and their value - cy.contains("genre: \"Classical\""); - cy.contains("sameAsUrl: "); - - // Check that the page loaded and that we can see the correct data - cy.contains("Finished in:"); - cy.contains("1-1 of 1"); - cy.get('.column-name').find('span').contains("Wolfgang Amadeus Mozart").should("exist");; - - // Check if the button to make a new query exists and use it - cy.get('button').contains("Change Variables").should("exist"); - cy.get('button').contains("Change Variables").click(); - - // Making sure we get the form to enter new variables - // and that the previously selected value(s) are still there - cy.get('.ra-input-genre').find('input').should('have.value', '"Classical"'); - cy.get('.ra-input-sameAsUrl').find('input').should('have.value', ''); - - // Change variables and make another existing combination - cy.get('.ra-input-genre').click(); - cy.get('li').contains('Baroque').click(); - - cy.get('.ra-input-sameAsUrl').click(); - cy.get('li').contains('Bach').click(); - - cy.get('button[type="submit"]').click(); - - cy.contains("Finished in:"); - cy.contains("1-1 of 1"); - cy.get('.column-name').find('span').contains("Johann Sebastian Bach").should("exist"); - - // check if the url is reflecting the new variable values - cy.url().should("have.string", "genre=%22Baroque%22"); - cy.url().should("have.string", "sameAsUrl=%3Chttps%3A%2F%2Fen.wikipedia.org%2Fwiki%2FJohann_Sebastian_Bach%3E"); - - // a second url - cy.visit("/#/1100?genre=%22Romantic%22&sameAsUrl=%3Chttps%3A%2F%2Fen.wikipedia.org%2Fwiki%2FFranz_Schubert%3E"); - - // Check the display of the variable(s) and their value - cy.contains("genre: \"Romantic\""); - cy.contains("sameAsUrl: "); - - // Check that the page loaded and that we can see the correct data - cy.contains("Finished in:"); - cy.contains("1-1 of 1"); - cy.get('.column-name').find('span').contains("Franz Schubert").should("exist");; - - // Check if the button to make a new query exists - cy.get('button').contains("Change Variables").should("exist"); - }); - it("Correct message displayed when no resulting data", () => { cy.visit("/"); cy.contains("Example queries").click(); @@ -245,4 +190,170 @@ describe("Templated query", () => { }); + it("Indirect with 1 variable", () => { + + cy.visit("/"); + cy.contains("Example queries").click(); + cy.contains("A templated query about musicians (indirect variables)").click(); + + // Fill in the form + cy.get('.ra-input-genre').click(); + cy.get('li').contains('Baroque').click(); + + // Comfirm query + cy.get('button').contains('Query').click(); + + // Check that the page loaded and that we can see the correct data + cy.contains("Finished in:"); + cy.get('.column-name').find('span').contains("Johann Sebastian Bach").should("exist");; + cy.get('.column-name').find('span').contains("Marc-Antoine Charpentier").should("exist");; + // Check that we don't see artists that don't belong here + cy.get('.column-name').find('span').contains("Franz Schubert").should("not.exist"); + cy.get('.column-name').find('span').contains("Wolfgang Amadeus Mozart").should("not.exist"); + + }); + + it("Indirect with 2 variables", () => { + + cy.visit("/"); + cy.contains("For testing only").click(); + cy.contains("A templated query about musicians, two variables (indirect variables)").click(); + + // Fill in the form + cy.get('.ra-input-genre').click(); + cy.get('li').contains('Classical').click(); + + cy.get('.ra-input-sameAsUrl').click(); + cy.get('li').contains('Mozart').click(); + + cy.get('button').contains('Query').click(); + + // Check that it is correctly loaded with and only the correct data appears + cy.contains("Finished in:"); + cy.get('.column-name').find('span').contains("Wolfgang Amadeus Mozart").should("exist");; + + cy.get('.column-name').find('span').contains("Franz Schubert").should("not.exist"); + cy.get('.column-name').find('span').contains("Johann Sebastian Bach").should("not.exist"); + cy.get('.column-name').find('span').contains("Ludwig van Beethoven").should("not.exist"); + + }); + + + it("Indirect with 1 variable and sources from indexfile", () => { + + cy.visit("/"); + cy.contains("For testing only").click(); + cy.contains("Component and materials - 1 variable (indirect source & indirect variables)").click(); + + // Fill in the form + cy.get('.ra-input-componentName').click(); + cy.get('li').contains('Component 1').click(); + + // Comfirm query + cy.get('button[type="submit"]').click(); + + // Check that it is correctly loaded with and only the correct data appears + cy.contains("Finished in:"); + + cy.get('.column-componentName').find('span').contains("Component 1").should("exist"); + cy.get('.column-materialName').find('span').contains("Material 2").should("exist"); + cy.get('.column-materialName').find('span').contains("Material 1").should("exist"); + + cy.get('.column-componentName').find('span').contains("Component 2").should("not.exist"); + cy.get('.column-componentName').find('span').contains("Component 3").should("not.exist"); + cy.get('.column-materialName').find('span').contains("Material 6").should("not.exist"); + + }); + + it("Indirect with 2 variables and sources from indexfile", () => { + cy.visit("/"); + cy.contains("For testing only").click(); + cy.contains("Component and materials - 2 variables (indirect source & indirect variables)").click(); + + // Fill in the form + cy.get('.ra-input-componentName').click(); + cy.get('li').contains('Component 1').click(); + + cy.get('.ra-input-materialName').click(); + cy.get('li').contains('Material 2').click(); + + // Comfirm query + cy.get('button[type="submit"]').click(); + + // Check that it is correctly loaded with and only the correct data appears + cy.contains("Finished in:"); + + cy.get('.column-componentName').find('span').contains("Component 1").should("exist"); + cy.get('.column-materialName').find('span').contains("Material 2").should("exist"); + + cy.get('.column-materialName').find('span').contains("Material 1").should("not.exist"); + cy.get('.column-componentName').find('span').contains("Component 2").should("not.exist"); + cy.get('.column-componentName').find('span').contains("Component 3").should("not.exist"); + cy.get('.column-materialName').find('span').contains("Material 6").should("not.exist"); + + }); + + it("Indirect with 1 variable but no sources available to get variables from", () => { + + cy.visit("/"); + cy.contains("For testing only").click(); + cy.contains("A templated query about musicians (indirect variables) - no sources").click(); + + cy.contains("Error getting variable options...").should("exist");; + }); + + it("Indirect with 2 variables - variable values from url search parameters", () => { + + // a first url + cy.visit("/#/9040?genre=%22Classical%22&sameAsUrl=%3Chttps%3A%2F%2Fen.wikipedia.org%2Fwiki%2FWolfgang_Amadeus_Mozart%3E"); + + // Check the display of the variable(s) and their value + cy.contains("genre: \"Classical\""); + cy.contains("sameAsUrl: "); + + // Check that the page loaded and that we can see the correct data + cy.contains("Finished in:"); + cy.contains("1-1 of 1"); + cy.get('.column-name').find('span').contains("Wolfgang Amadeus Mozart").should("exist");; + + // Check if the button to make a new query exists and use it + cy.get('button').contains("Change Variables").should("exist"); + cy.get('button').contains("Change Variables").click(); + + // Making sure we get the form to enter new variables + // and that the previously selected value(s) are still there + cy.get('.ra-input-genre').find('input').should('have.value', '"Classical"'); + cy.get('.ra-input-sameAsUrl').find('input').should('have.value', ''); + + // Change variables and make another existing combination + cy.get('.ra-input-genre').click(); + cy.get('li').contains('Baroque').click(); + + cy.get('.ra-input-sameAsUrl').click(); + cy.get('li').contains('Bach').click(); + + cy.get('button[type="submit"]').click(); + + cy.contains("Finished in:"); + cy.contains("1-1 of 1"); + cy.get('.column-name').find('span').contains("Johann Sebastian Bach").should("exist"); + + // check if the url is reflecting the new variable values + cy.url().should("have.string", "genre=%22Baroque%22"); + cy.url().should("have.string", "sameAsUrl=%3Chttps%3A%2F%2Fen.wikipedia.org%2Fwiki%2FJohann_Sebastian_Bach%3E"); + + // a second url + cy.visit("/#/9040?genre=%22Romantic%22&sameAsUrl=%3Chttps%3A%2F%2Fen.wikipedia.org%2Fwiki%2FFranz_Schubert%3E"); + + // Check the display of the variable(s) and their value + cy.contains("genre: \"Romantic\""); + cy.contains("sameAsUrl: "); + + // Check that the page loaded and that we can see the correct data + cy.contains("Finished in:"); + cy.contains("1-1 of 1"); + cy.get('.column-name').find('span').contains("Franz Schubert").should("exist");; + + }); + });