From 484f3cc3517b4cdcc5942ac2d95012b58cf9aed8 Mon Sep 17 00:00:00 2001 From: EmilioTR Date: Thu, 8 Aug 2024 13:32:17 +0200 Subject: [PATCH 01/11] moved the custom components to appropriate place --- src/App.jsx | 2 +- .../{Dashboard => }/CustomQueryEditor/customEditor.jsx | 4 ++-- .../CustomQueryEditor/customQueryEditButton.jsx | 4 ++-- src/components/ListResultTable/TemplatedQueryForm.jsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/components/{Dashboard => }/CustomQueryEditor/customEditor.jsx (99%) rename src/components/{Dashboard => }/CustomQueryEditor/customQueryEditButton.jsx (97%) diff --git a/src/App.jsx b/src/App.jsx index dabc1c44..e9cf6daa 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,7 +15,7 @@ import InteractionLayout from "./components/InteractionLayout/InteractionLayout" import TemplatedListResultTable from "./components/ListResultTable/TemplatedListResultTable.jsx"; import { Route } from "react-router-dom"; -import CustomEditor from "./components/Dashboard/CustomQueryEditor/customEditor.jsx"; +import CustomEditor from "./components/CustomQueryEditor/customEditor.jsx"; import configManager from "./configManager/configManager.js"; diff --git a/src/components/Dashboard/CustomQueryEditor/customEditor.jsx b/src/components/CustomQueryEditor/customEditor.jsx similarity index 99% rename from src/components/Dashboard/CustomQueryEditor/customEditor.jsx rename to src/components/CustomQueryEditor/customEditor.jsx index 5492c201..342ab460 100644 --- a/src/components/Dashboard/CustomQueryEditor/customEditor.jsx +++ b/src/components/CustomQueryEditor/customEditor.jsx @@ -7,8 +7,8 @@ import { CardActions, Typography } from '@mui/material'; import { useLocation, useNavigate } from 'react-router-dom'; import FormControlLabel from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; -import configManager from '../../../configManager/configManager'; -import IconProvider from '../../../IconProvider/IconProvider'; +import configManager from '../../configManager/configManager'; +import IconProvider from '../../IconProvider/IconProvider'; export default function CustomEditor(props) { diff --git a/src/components/Dashboard/CustomQueryEditor/customQueryEditButton.jsx b/src/components/CustomQueryEditor/customQueryEditButton.jsx similarity index 97% rename from src/components/Dashboard/CustomQueryEditor/customQueryEditButton.jsx rename to src/components/CustomQueryEditor/customQueryEditButton.jsx index 905c4848..e9950b38 100644 --- a/src/components/Dashboard/CustomQueryEditor/customQueryEditButton.jsx +++ b/src/components/CustomQueryEditor/customQueryEditButton.jsx @@ -7,8 +7,8 @@ import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; import Box from '@mui/material/Box'; import { useNavigate } from 'react-router-dom'; -import IconProvider from '../../../IconProvider/IconProvider'; -import configManager from '../../../configManager/configManager'; +import IconProvider from '../../IconProvider/IconProvider'; +import configManager from '../../configManager/configManager'; import TextField from '@mui/material/TextField'; diff --git a/src/components/ListResultTable/TemplatedQueryForm.jsx b/src/components/ListResultTable/TemplatedQueryForm.jsx index 24135424..9f55e605 100644 --- a/src/components/ListResultTable/TemplatedQueryForm.jsx +++ b/src/components/ListResultTable/TemplatedQueryForm.jsx @@ -2,7 +2,7 @@ import {Toolbar, SaveButton, SelectInput, SimpleForm, required, useResourceDefin import DoneIcon from '@mui/icons-material/Done'; import {Component, useEffect} from "react"; import PropTypes from "prop-types"; -import CustomQueryEditButton from "../Dashboard/CustomQueryEditor/customQueryEditButton"; +import CustomQueryEditButton from "../CustomQueryEditor/customQueryEditButton"; const MyToolbar = () => ( From 3256dde5dba38262eaa3d3cd833ae8aee9d91be5 Mon Sep 17 00:00:00 2001 From: EmilioTR Date: Thu, 8 Aug 2024 13:53:33 +0200 Subject: [PATCH 02/11] first version of the converting button --- src/IconProvider/IconProvider.js | 4 +- .../customConversionButton.jsx | 171 ++++++++++++++++++ .../QueryResultList/QueryResultList.jsx | 5 +- 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/components/CustomQueryEditor/customConversionButton.jsx diff --git a/src/IconProvider/IconProvider.js b/src/IconProvider/IconProvider.js index 9b5a2d4b..b98975ff 100644 --- a/src/IconProvider/IconProvider.js +++ b/src/IconProvider/IconProvider.js @@ -21,6 +21,7 @@ import TuneIcon from '@mui/icons-material/Tune'; import SaveAsIcon from '@mui/icons-material/SaveAs'; import InfoIcon from '@mui/icons-material/Info'; import CloseIcon from '@mui/icons-material/Close'; +import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest'; export default { BrushIcon, @@ -45,5 +46,6 @@ export default { TuneIcon, SaveAsIcon, InfoIcon, - CloseIcon + CloseIcon, + SettingsSuggestIcon }; diff --git a/src/components/CustomQueryEditor/customConversionButton.jsx b/src/components/CustomQueryEditor/customConversionButton.jsx new file mode 100644 index 00000000..82a1ba68 --- /dev/null +++ b/src/components/CustomQueryEditor/customConversionButton.jsx @@ -0,0 +1,171 @@ +import { useNavigate } from 'react-router-dom'; + +import configManager from "../../configManager/configManager"; +import Button from '@mui/material/Button'; +import IconProvider from "../../IconProvider/IconProvider"; + + +export default function CustomConversionButton({ id }) { + + const navigate = useNavigate(); + const config = configManager.getConfig() + const queryToConvert = configManager.getQueryById(id); + const newId = Date.now().toString() + + const adaptedQuery = { + ...queryToConvert, + id: newId, + queryGroupId: "cstm", + } + + // This function replaces the queryLocation with a querystring, which makes it adapted for a custom query + async function replaceQueryLocationWithQueryString() { + + const queryString = await configManager.getQueryText(adaptedQuery) + + delete adaptedQuery.queryLocation + adaptedQuery.queryString = queryString + + + } + + // This function handles the conversion of the comunica sources and context + function convertComunicaContextAndSources() { + + if (adaptedQuery.comunicaContext) { + if (adaptedQuery.comunicaContext.sources) { + adaptedQuery.source = adaptedQuery.comunicaContext.sources.join(' ; ') + } + + const keys = Object.keys(adaptedQuery.comunicaContext); + const otherKeys = keys.filter(key => key !== 'sources'); + + const hasOtherProperties = otherKeys.length > 0; + if (hasOtherProperties) { + adaptedQuery.comunicaContextCheck = "on" + } + } + } + + // This function handles the conversion of indirect sources + async function convertSourcesFromSourceIndex() { + if (adaptedQuery.sourcesIndex){ + + adaptedQuery.sourceIndexCheck = "on"; + + const queryString = await configManager.getQueryText(adaptedQuery.sourcesIndex) + + console.log(adaptedQuery.sourcesIndex.url) + adaptedQuery.indexSourceUrl = adaptedQuery.sourcesIndex.url + adaptedQuery.indexSourceQuery = queryString + + delete adaptedQuery.sourcesIndex.queryLocation + adaptedQuery.sourcesIndex.queryString = queryString + + } + } + + // This function handles the conversion for fixed variables + function convertTemplatedQueriesFixedVariables() { + if(adaptedQuery.variables){ + adaptedQuery.directVariablesCheck = "on" + } + } + + + // This function handles the conversion for indirect variables + async function convertTemplatedQueriesIndirectVariables() { + let queryStringList = []; + + if(adaptedQuery.indirectVariables){ + adaptedQuery.indirectVariablesCheck = "on" + + if(adaptedQuery.indirectVariables.queryLocations){ + for (const location of adaptedQuery.indirectVariables.queryLocations) { + + const result = await fetch(`${config.queryFolder}${location}`); + const queryStr = await result.text(); + + queryStringList.push(queryStr); + } + } + if (adaptedQuery.indirectVariables.queryStrings) { + queryStringList = [...queryStringList, ...adaptedQuery.indirectVariables.queryStrings]; + } + + adaptedQuery.indirectQueries = queryStringList + + } + } + + // This function handles the conversion of an ask query + function convertASKquery() { + if (adaptedQuery.askQuery){ + adaptedQuery.askQueryCheck = "on" + } + } + + function handleSearchParams(queryToHandle) { + + const copyObject = JSON.parse(JSON.stringify(queryToHandle)); + + if( copyObject.comunicaContextCheck !== "on"){ + delete copyObject.comunicaContext + } else { + delete copyObject.comunicaContext.sources + } + + for (let content in copyObject) { + if (typeof copyObject[content] === 'object') { + copyObject[content] = JSON.stringify(copyObject[content]) + } + + } + + return new URLSearchParams(copyObject); + } + + + const convertQueryToCustom = async () => { + + + // logic + + await replaceQueryLocationWithQueryString() + await convertSourcesFromSourceIndex() + await convertTemplatedQueriesIndirectVariables() + + convertTemplatedQueriesFixedVariables() + convertComunicaContextAndSources() + convertASKquery() + + + + + configManager.addNewQueryGroup('cstm', 'Custom queries', 'EditNoteIcon'); + + + adaptedQuery.searchParams = handleSearchParams(adaptedQuery) + + // const searchParams = new URLSearchParams(adaptedQuery); + + // adaptedQuery.searchParams = searchParams + + configManager.addQuery(adaptedQuery) + + navigate("/" + newId) + } + + return ( + + + ) +} \ No newline at end of file diff --git a/src/components/ListResultTable/QueryResultList/QueryResultList.jsx b/src/components/ListResultTable/QueryResultList/QueryResultList.jsx index f483a17e..55f864bf 100644 --- a/src/components/ListResultTable/QueryResultList/QueryResultList.jsx +++ b/src/components/ListResultTable/QueryResultList/QueryResultList.jsx @@ -8,9 +8,10 @@ import Button from '@mui/material/Button'; import SearchOffIcon from '@mui/icons-material/SearchOff'; import { SvgIcon, Box, Typography } from "@mui/material"; import PropTypes from "prop-types"; -import CustomQueryEditButton from "../../Dashboard/CustomQueryEditor/customQueryEditButton"; +import CustomQueryEditButton from "../../CustomQueryEditor/customQueryEditButton"; import IconProvider from "../../../IconProvider/IconProvider"; import configManager from "../../../configManager/configManager"; +import CustomConversionButton from "../../CustomQueryEditor/customConversionButton"; /** * @param {object} props - the props passed down to the component @@ -39,7 +40,7 @@ function QueryResultList(props) { <div style={{ display: 'flex', flexDirection: 'row' }}> {submitted && <Aside changeVariables={changeVariables} />} - {resourceDef.options.queryGroupId === 'cstm' && <CustomQueryEditButton queryID={resourceDef.name} submitted={submitted} />} + {resourceDef.options.queryGroupId === 'cstm' ? <CustomQueryEditButton queryID={resourceDef.name} submitted={submitted} /> : <CustomConversionButton query={query} id={resourceDef.name}/>} </div> <Typography fontSize={"2rem"} mt={2} > {queryTitle} </Typography> {values ? ( From c387e8d5b6d42195b6861ea18b3665ed5e5ae9da Mon Sep 17 00:00:00 2001 From: EmilioTR <Emilio.TenaRomero@hotmail.com> Date: Thu, 8 Aug 2024 15:34:21 +0200 Subject: [PATCH 03/11] code is more clear + navigate to the custom form --- .../customConversionButton.jsx | 216 ++++++++---------- 1 file changed, 99 insertions(+), 117 deletions(-) diff --git a/src/components/CustomQueryEditor/customConversionButton.jsx b/src/components/CustomQueryEditor/customConversionButton.jsx index 82a1ba68..7192fbe5 100644 --- a/src/components/CustomQueryEditor/customConversionButton.jsx +++ b/src/components/CustomQueryEditor/customConversionButton.jsx @@ -1,171 +1,153 @@ import { useNavigate } from 'react-router-dom'; - import configManager from "../../configManager/configManager"; -import Button from '@mui/material/Button'; import IconProvider from "../../IconProvider/IconProvider"; - +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; export default function CustomConversionButton({ id }) { const navigate = useNavigate(); const config = configManager.getConfig() - const queryToConvert = configManager.getQueryById(id); - const newId = Date.now().toString() - const adaptedQuery = { - ...queryToConvert, - id: newId, - queryGroupId: "cstm", + // We have to make sure we have a deep copy, and not a shallow copy of the original query + const convertedQuery = JSON.parse(JSON.stringify(configManager.getQueryById(id))); + + + const convertQueryToCustom = async () => { + + /* + The convertion logic is split up in apart functions to enhance its clarity. + */ + + await replaceQueryLocationWithQueryString() + await convertSourcesFromSourceIndex() + await convertTemplatedQueriesIndirectVariables() + + convertTemplatedQueriesFixedVariables() + convertComunicaContextAndSources() + convertASKquery() + + // The id and group have to be deleted because a new custom query is going to be made. Name is modified. + delete convertedQuery.id + delete convertedQuery.queryGroupId + convertedQuery.name = `(Derived from) ${convertedQuery.name}` + + // Generate the search parameters so that we can create a custom query. + const searchParams = handleSearchParams(convertedQuery) + navigate(`/customQuery?${searchParams.toString()}`) } - // This function replaces the queryLocation with a querystring, which makes it adapted for a custom query - async function replaceQueryLocationWithQueryString() { - const queryString = await configManager.getQueryText(adaptedQuery) + // Handles the objects parsing and ensure the correct comunica structure is given + function handleSearchParams(queryToHandle) { + + const copyObject = JSON.parse(JSON.stringify(queryToHandle)); - delete adaptedQuery.queryLocation - adaptedQuery.queryString = queryString + for (let content in copyObject) { + if (typeof copyObject[content] === 'object') { + copyObject[content] = JSON.stringify(copyObject[content]) + } + } + return new URLSearchParams(copyObject); + } + // This function replaces the queryLocation with a querystring, which makes it adapted for a custom query + async function replaceQueryLocationWithQueryString() { + const queryString = await configManager.getQueryText(convertedQuery) + convertedQuery.queryString = queryString + delete convertedQuery.queryLocation } // This function handles the conversion of the comunica sources and context function convertComunicaContextAndSources() { - if (adaptedQuery.comunicaContext) { - if (adaptedQuery.comunicaContext.sources) { - adaptedQuery.source = adaptedQuery.comunicaContext.sources.join(' ; ') + if (convertedQuery.comunicaContext) { + if (convertedQuery.comunicaContext.sources) { + convertedQuery.source = convertedQuery.comunicaContext.sources.join(' ; ') } - const keys = Object.keys(adaptedQuery.comunicaContext); + const keys = Object.keys(convertedQuery.comunicaContext); const otherKeys = keys.filter(key => key !== 'sources'); - const hasOtherProperties = otherKeys.length > 0; + if (hasOtherProperties) { - adaptedQuery.comunicaContextCheck = "on" + convertedQuery.comunicaContextCheck = "on" + delete convertedQuery.comunicaContext.sources + } else { + delete convertedQuery.comunicaContext } + } } // This function handles the conversion of indirect sources async function convertSourcesFromSourceIndex() { - if (adaptedQuery.sourcesIndex){ - - adaptedQuery.sourceIndexCheck = "on"; + if (convertedQuery.sourcesIndex) { - const queryString = await configManager.getQueryText(adaptedQuery.sourcesIndex) + convertedQuery.sourceIndexCheck = "on"; + const queryString = await configManager.getQueryText(convertedQuery.sourcesIndex) - console.log(adaptedQuery.sourcesIndex.url) - adaptedQuery.indexSourceUrl = adaptedQuery.sourcesIndex.url - adaptedQuery.indexSourceQuery = queryString - - delete adaptedQuery.sourcesIndex.queryLocation - adaptedQuery.sourcesIndex.queryString = queryString - - } - } - - // This function handles the conversion for fixed variables - function convertTemplatedQueriesFixedVariables() { - if(adaptedQuery.variables){ - adaptedQuery.directVariablesCheck = "on" + convertedQuery.indexSourceUrl = convertedQuery.sourcesIndex.url + convertedQuery.indexSourceQuery = queryString + delete convertedQuery.sourcesIndex.queryLocation } } - + + // This function handles the conversion for indirect variables async function convertTemplatedQueriesIndirectVariables() { let queryStringList = []; - - if(adaptedQuery.indirectVariables){ - adaptedQuery.indirectVariablesCheck = "on" - - if(adaptedQuery.indirectVariables.queryLocations){ - for (const location of adaptedQuery.indirectVariables.queryLocations) { - + + if (convertedQuery.indirectVariables) { + convertedQuery.indirectVariablesCheck = "on" + + if (convertedQuery.indirectVariables.queryLocations) { + for (const location of convertedQuery.indirectVariables.queryLocations) { + const result = await fetch(`${config.queryFolder}${location}`); const queryStr = await result.text(); - + queryStringList.push(queryStr); - } - } - if (adaptedQuery.indirectVariables.queryStrings) { - queryStringList = [...queryStringList, ...adaptedQuery.indirectVariables.queryStrings]; - } - - adaptedQuery.indirectQueries = queryStringList - + } + } + if (convertedQuery.indirectVariables.queryStrings) { + queryStringList = [...queryStringList, ...convertedQuery.indirectVariables.queryStrings]; + } + + convertedQuery.indirectQueries = queryStringList + } } - // This function handles the conversion of an ask query - function convertASKquery() { - if (adaptedQuery.askQuery){ - adaptedQuery.askQueryCheck = "on" + // This function handles the logic for fixed variables + function convertTemplatedQueriesFixedVariables() { + if (convertedQuery.variables) { + convertedQuery.directVariablesCheck = "on" } } - - function handleSearchParams(queryToHandle) { - - const copyObject = JSON.parse(JSON.stringify(queryToHandle)); - - if( copyObject.comunicaContextCheck !== "on"){ - delete copyObject.comunicaContext - } else { - delete copyObject.comunicaContext.sources - } - - for (let content in copyObject) { - if (typeof copyObject[content] === 'object') { - copyObject[content] = JSON.stringify(copyObject[content]) - } - + + // This function handles the logic for the ask query + function convertASKquery() { + if (convertedQuery.askQuery) { + convertedQuery.askQueryCheck = "on" } - - return new URLSearchParams(copyObject); } - const convertQueryToCustom = async () => { - - - // logic - - await replaceQueryLocationWithQueryString() - await convertSourcesFromSourceIndex() - await convertTemplatedQueriesIndirectVariables() - - convertTemplatedQueriesFixedVariables() - convertComunicaContextAndSources() - convertASKquery() - - - - - configManager.addNewQueryGroup('cstm', 'Custom queries', 'EditNoteIcon'); - - - adaptedQuery.searchParams = handleSearchParams(adaptedQuery) - - // const searchParams = new URLSearchParams(adaptedQuery); - - // adaptedQuery.searchParams = searchParams - - configManager.addQuery(adaptedQuery) - - navigate("/" + newId) - } - return ( - <Button - variant="outlined" - color="warning" - onClick={convertQueryToCustom} - type="button" - startIcon={<IconProvider.SettingsSuggestIcon />} - > - Customize - </Button> - + <Box display="flex" justifyContent="flex-end" width="100%"> + <Button + variant="outlined" + color="warning" + onClick={convertQueryToCustom} + type="button" + startIcon={<IconProvider.SettingsSuggestIcon />} + sx={{ marginTop: '10px' , marginBottom: '10px'}} + > + Customize + </Button> + </Box> ) } \ No newline at end of file From 3f1f373a6370b9eb379ab942c96c45e331533f7a Mon Sep 17 00:00:00 2001 From: EmilioTR <Emilio.TenaRomero@hotmail.com> Date: Thu, 8 Aug 2024 16:29:07 +0200 Subject: [PATCH 04/11] test for the feature --- cypress/e2e/customize-existing-query.cy.js | 166 +++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 cypress/e2e/customize-existing-query.cy.js diff --git a/cypress/e2e/customize-existing-query.cy.js b/cypress/e2e/customize-existing-query.cy.js new file mode 100644 index 00000000..1ea26d77 --- /dev/null +++ b/cypress/e2e/customize-existing-query.cy.js @@ -0,0 +1,166 @@ +describe("Customize existing query", () => { + + it("simple query", () => { + cy.visit("/"); + cy.contains("General examples").click(); + cy.contains("A public list of books I'd love to own").click(); + + cy.get('button').contains("Customize").click(); + + cy.url().should('include', 'customQuery'); + + + cy.get('input[name="name"]').should('have.value', "(Derived from) A public list of books I'd love to own"); + + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> + +SELECT * WHERE { + ?list schema:name ?listTitle; + schema:itemListElement [ + schema:name ?bookTitle; + schema:creator [ + schema:name ?authorName + ] + ]. +}`); + cy.get('input[name="source"]').should('have.value', "http://localhost:8080/example/wish-list"); + + }) + + it("templated query - fixed variables", () => { + cy.visit("/"); + cy.contains("General examples").click(); + cy.contains("A templated query about musicians").click(); + + cy.get('form').within(() => { + cy.get('#genre').click(); + }); + cy.get('li').contains('Baroque').click(); + + cy.get('button[type="submit"]').click(); + + cy.get('button').contains("Customize").click(); + cy.url().should('include', 'customQuery'); + + cy.get('input[name="name"]').should('have.value', "(Derived from) A templated query about musicians"); + + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> + +SELECT ?name ?sameAs_url WHERE { + ?list schema:name ?listTitle; + schema:name ?name; + schema:genre $genre; + schema:sameAs ?sameAs_url; +}`); + + cy.get('textarea[name="variables"]').should('have.value', `{"genre":["\\"Romantic\\"","\\"Baroque\\"","\\"Classical\\""]}`) + + + + + + }) + + it("templated query - indirect variables", () => { + + cy.visit("/"); + cy.contains("For testing only").click(); + cy.contains("A templated query about musicians, two variables (indirect variables)").click(); + + cy.get('form').within(() => { + cy.get('#genre').click(); + }); + cy.get('li').contains('Baroque').click(); + + cy.get('form').within(() => { + cy.get('#sameAsUrl').click(); + }); + cy.get('li').contains('Vivaldi').click(); + + cy.get('button[type="submit"]').click(); + + cy.get('button').contains("Customize").click(); + cy.url().should('include', 'customQuery'); + + cy.get('input[name="name"]').should('have.value', "(Derived from) A templated query about musicians, two variables (indirect variables)"); + + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> + +SELECT ?name WHERE { + ?list schema:name ?listTitle; + schema:name ?name; + schema:genre $genre; + schema:sameAs $sameAsUrl; +} +`); + + cy.get('textarea[name="indirectQuery1"]').should('have.value', `PREFIX schema: <http://schema.org/> + +SELECT DISTINCT ?genre +WHERE { + ?list schema:genre ?genre +} +ORDER BY ?genre +`); + + cy.get('textarea[name="indirectQuery2"]').should('have.value', `PREFIX schema: <http://schema.org/> + +SELECT DISTINCT ?sameAsUrl +WHERE { + ?list schema:sameAs ?sameAsUrl +} +ORDER BY ?sameAsUrl +`); + + + + + }) + + it("index file", () => { + cy.visit("/"); + cy.contains("General examples").click(); + cy.contains("Sources from an index file").click(); + + cy.get('button').contains("Customize").click({force:true}); // Button is out of FoV so we gotta force the click + + cy.url().should('include', 'customQuery'); + + + cy.get('input[name="name"]').should('have.value', "(Derived from) Sources from an index file"); + + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX o: <https://www.example.com/ont/> + +SELECT ?component ?componentName ?material ?materialName ?percentage +WHERE { + ?component + a o:Component ; + o:name ?componentName ; + o:has-component-bom [ + o:has-component-material-assoc [ + o:percentage ?percentage ; + o:has-material ?material ; + ]; + ]; + . + ?material o:name ?materialName ; +} +ORDER BY ?componentName +`); + + cy.get('input[name="indexSourceUrl"]').should('have.value', `http://localhost:8080/example/index-example-texon-only`) + cy.get('textarea[name="indexSourceQuery"]').should('have.value', `PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX example: <http://localhost:8080/example/index-example-texon-only#> + +SELECT ?object +WHERE { + example:index-example rdfs:seeAlso ?object . +} +`) + + }) + + +}) \ No newline at end of file From aa421114033529071087dabec3c263209a6278d4 Mon Sep 17 00:00:00 2001 From: EmilioTR <Emilio.TenaRomero@hotmail.com> Date: Mon, 19 Aug 2024 10:08:17 +0200 Subject: [PATCH 05/11] Updated UI --- src/IconProvider/IconProvider.js | 4 +- .../CustomQueryEditor/customEditor.jsx | 37 ++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/IconProvider/IconProvider.js b/src/IconProvider/IconProvider.js index b98975ff..19e08d8f 100644 --- a/src/IconProvider/IconProvider.js +++ b/src/IconProvider/IconProvider.js @@ -22,6 +22,7 @@ import SaveAsIcon from '@mui/icons-material/SaveAs'; import InfoIcon from '@mui/icons-material/Info'; import CloseIcon from '@mui/icons-material/Close'; import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; export default { BrushIcon, @@ -47,5 +48,6 @@ export default { SaveAsIcon, InfoIcon, CloseIcon, - SettingsSuggestIcon + SettingsSuggestIcon, + ChevronLeftIcon }; diff --git a/src/components/CustomQueryEditor/customEditor.jsx b/src/components/CustomQueryEditor/customEditor.jsx index 342ab460..81334ed8 100644 --- a/src/components/CustomQueryEditor/customEditor.jsx +++ b/src/components/CustomQueryEditor/customEditor.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import { CardActions, Typography } from '@mui/material'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -545,17 +546,35 @@ ORDER BY ?genre`; )} <CardActions> + <Box display="flex" justifyContent="space-between" width="100%" sx={{ marginX: '10px' }}> + <Button variant="contained" type="submit" startIcon={props.newQuery ? <IconProvider.AddIcon /> : <IconProvider.SaveAsIcon />}> + {props.newQuery ? 'Create Query' : 'Save Changes'} + </Button> - <Button variant="contained" type="submit" startIcon={props.newQuery ? <IconProvider.AddIcon /> : <IconProvider.SaveAsIcon />}> - {props.newQuery ? 'Create Query' : 'Save Changes'} - </Button> + { + props.newQuery ? - { - props.newQuery ? null : - <Button variant="outlined" color='error' onClick={() => { navigate(`/${props.id}/`) }} startIcon={<IconProvider.CloseIcon />}> - {'Cancel'} - </Button> - } + <Button + variant="outlined" + onClick={() => { navigate(-1) }} + startIcon={<IconProvider.ChevronLeftIcon />} + > + Go Back + </Button> + + : + + <Button + variant="outlined" + color='error' + onClick={() => { navigate(`/${props.id}/`) }} + startIcon={<IconProvider.CloseIcon />} + > + Cancel + </Button> + } + + </Box> </CardActions> </Card> From 427157e33f855a98250f00df20010eca5a6f10c4 Mon Sep 17 00:00:00 2001 From: EmilioTR <Emilio.TenaRomero@hotmail.com> Date: Mon, 19 Aug 2024 14:34:17 +0200 Subject: [PATCH 06/11] added duplication functionality --- cypress/e2e/customize-existing-query.cy.js | 154 +++++++++++------- src/IconProvider/IconProvider.js | 4 +- .../customConversionButton.jsx | 4 +- .../customQueryEditButton.jsx | 75 ++++++--- 4 files changed, 152 insertions(+), 85 deletions(-) diff --git a/cypress/e2e/customize-existing-query.cy.js b/cypress/e2e/customize-existing-query.cy.js index 1ea26d77..f83e397a 100644 --- a/cypress/e2e/customize-existing-query.cy.js +++ b/cypress/e2e/customize-existing-query.cy.js @@ -1,18 +1,18 @@ describe("Customize existing query", () => { - it("simple query", () => { - cy.visit("/"); - cy.contains("General examples").click(); - cy.contains("A public list of books I'd love to own").click(); + it("simple query", () => { + cy.visit("/"); + cy.contains("General examples").click(); + cy.contains("A public list of books I'd love to own").click(); - cy.get('button').contains("Customize").click(); + cy.get('button').contains("Clone as custom query").click(); - cy.url().should('include', 'customQuery'); + cy.url().should('include', 'customQuery'); - cy.get('input[name="name"]').should('have.value', "(Derived from) A public list of books I'd love to own"); + cy.get('input[name="name"]').should('have.value', "(Cloned from) A public list of books I'd love to own"); - cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> SELECT * WHERE { ?list schema:name ?listTitle; @@ -23,28 +23,28 @@ SELECT * WHERE { ] ]. }`); - cy.get('input[name="source"]').should('have.value', "http://localhost:8080/example/wish-list"); + cy.get('input[name="source"]').should('have.value', "http://localhost:8080/example/wish-list"); - }) + }) - it("templated query - fixed variables", () => { - cy.visit("/"); - cy.contains("General examples").click(); - cy.contains("A templated query about musicians").click(); + it("templated query - fixed variables", () => { + cy.visit("/"); + cy.contains("General examples").click(); + cy.contains("A templated query about musicians").click(); - cy.get('form').within(() => { - cy.get('#genre').click(); - }); - cy.get('li').contains('Baroque').click(); + cy.get('form').within(() => { + cy.get('#genre').click(); + }); + cy.get('li').contains('Baroque').click(); - cy.get('button[type="submit"]').click(); + cy.get('button[type="submit"]').click(); - cy.get('button').contains("Customize").click(); - cy.url().should('include', 'customQuery'); + cy.get('button').contains("Clone as custom query").click(); + cy.url().should('include', 'customQuery'); - cy.get('input[name="name"]').should('have.value', "(Derived from) A templated query about musicians"); + cy.get('input[name="name"]').should('have.value', "(Cloned from) A templated query about musicians"); - cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> SELECT ?name ?sameAs_url WHERE { ?list schema:name ?listTitle; @@ -53,38 +53,38 @@ SELECT ?name ?sameAs_url WHERE { schema:sameAs ?sameAs_url; }`); - cy.get('textarea[name="variables"]').should('have.value', `{"genre":["\\"Romantic\\"","\\"Baroque\\"","\\"Classical\\""]}`) + cy.get('textarea[name="variables"]').should('have.value', `{"genre":["\\"Romantic\\"","\\"Baroque\\"","\\"Classical\\""]}`) - }) + }) - it("templated query - indirect variables", () => { + it("templated query - indirect variables", () => { - cy.visit("/"); - cy.contains("For testing only").click(); - cy.contains("A templated query about musicians, two variables (indirect variables)").click(); + cy.visit("/"); + cy.contains("For testing only").click(); + cy.contains("A templated query about musicians, two variables (indirect variables)").click(); - cy.get('form').within(() => { - cy.get('#genre').click(); - }); - cy.get('li').contains('Baroque').click(); + cy.get('form').within(() => { + cy.get('#genre').click(); + }); + cy.get('li').contains('Baroque').click(); - cy.get('form').within(() => { - cy.get('#sameAsUrl').click(); - }); - cy.get('li').contains('Vivaldi').click(); + cy.get('form').within(() => { + cy.get('#sameAsUrl').click(); + }); + cy.get('li').contains('Vivaldi').click(); - cy.get('button[type="submit"]').click(); + cy.get('button[type="submit"]').click(); - cy.get('button').contains("Customize").click(); - cy.url().should('include', 'customQuery'); + cy.get('button').contains("Clone as custom query").click(); + cy.url().should('include', 'customQuery'); - cy.get('input[name="name"]').should('have.value', "(Derived from) A templated query about musicians, two variables (indirect variables)"); + cy.get('input[name="name"]').should('have.value', "(Cloned from) A templated query about musicians, two variables (indirect variables)"); - cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> SELECT ?name WHERE { ?list schema:name ?listTitle; @@ -94,7 +94,7 @@ SELECT ?name WHERE { } `); - cy.get('textarea[name="indirectQuery1"]').should('have.value', `PREFIX schema: <http://schema.org/> + cy.get('textarea[name="indirectQuery1"]').should('have.value', `PREFIX schema: <http://schema.org/> SELECT DISTINCT ?genre WHERE { @@ -103,7 +103,7 @@ WHERE { ORDER BY ?genre `); - cy.get('textarea[name="indirectQuery2"]').should('have.value', `PREFIX schema: <http://schema.org/> + cy.get('textarea[name="indirectQuery2"]').should('have.value', `PREFIX schema: <http://schema.org/> SELECT DISTINCT ?sameAsUrl WHERE { @@ -115,21 +115,21 @@ ORDER BY ?sameAsUrl - }) + }) - it("index file", () => { - cy.visit("/"); - cy.contains("General examples").click(); - cy.contains("Sources from an index file").click(); + it("index file", () => { + cy.visit("/"); + cy.contains("General examples").click(); + cy.contains("Sources from an index file").click(); - cy.get('button').contains("Customize").click({force:true}); // Button is out of FoV so we gotta force the click + cy.get('button').contains("Clone as custom query").click({ force: true }); // Button is out of FoV so we gotta force the click - cy.url().should('include', 'customQuery'); + cy.url().should('include', 'customQuery'); - cy.get('input[name="name"]').should('have.value', "(Derived from) Sources from an index file"); + cy.get('input[name="name"]').should('have.value', "(Cloned from) Sources from an index file"); - cy.get('textarea[name="queryString"]').should('have.value', `PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX o: <https://www.example.com/ont/> SELECT ?component ?componentName ?material ?materialName ?percentage @@ -149,8 +149,8 @@ WHERE { ORDER BY ?componentName `); - cy.get('input[name="indexSourceUrl"]').should('have.value', `http://localhost:8080/example/index-example-texon-only`) - cy.get('textarea[name="indexSourceQuery"]').should('have.value', `PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> + cy.get('input[name="indexSourceUrl"]').should('have.value', `http://localhost:8080/example/index-example-texon-only`) + cy.get('textarea[name="indexSourceQuery"]').should('have.value', `PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX example: <http://localhost:8080/example/index-example-texon-only#> @@ -160,7 +160,49 @@ WHERE { } `) - }) + }) +}) + +describe("Clone and customize existing query, clone the custom after", () => { + + it("clone simple query", () => { + cy.visit("/"); + cy.contains("General examples").click(); + cy.contains("A public list of books I'd love to own").click(); + + cy.get('button').contains("Clone as custom query").click(); + + cy.url().should('include', 'customQuery'); + + + cy.get('input[name="name"]').should('have.value', "(Cloned from) A public list of books I'd love to own"); + + cy.get('textarea[name="queryString"]').should('have.value', `PREFIX schema: <http://schema.org/> + +SELECT * WHERE { + ?list schema:name ?listTitle; + schema:itemListElement [ + schema:name ?bookTitle; + schema:creator [ + schema:name ?authorName + ] + ]. +}` + ); + cy.get('input[name="source"]').should('have.value', "http://localhost:8080/example/wish-list"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Colleen Hoover").should("exist"); + + cy.get('button').contains("Clone").click(); + + cy.url().should('include', 'customQuery'); + + + cy.get('input[name="name"]').should('have.value', "(Cloned) (Cloned from) A public list of books I'd love to own"); + + }) }) \ No newline at end of file diff --git a/src/IconProvider/IconProvider.js b/src/IconProvider/IconProvider.js index 19e08d8f..0b4fc878 100644 --- a/src/IconProvider/IconProvider.js +++ b/src/IconProvider/IconProvider.js @@ -23,6 +23,7 @@ import InfoIcon from '@mui/icons-material/Info'; import CloseIcon from '@mui/icons-material/Close'; import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import FilterNoneIcon from '@mui/icons-material/FilterNone'; export default { BrushIcon, @@ -49,5 +50,6 @@ export default { InfoIcon, CloseIcon, SettingsSuggestIcon, - ChevronLeftIcon + ChevronLeftIcon, + FilterNoneIcon }; diff --git a/src/components/CustomQueryEditor/customConversionButton.jsx b/src/components/CustomQueryEditor/customConversionButton.jsx index 7192fbe5..e06b0267 100644 --- a/src/components/CustomQueryEditor/customConversionButton.jsx +++ b/src/components/CustomQueryEditor/customConversionButton.jsx @@ -30,7 +30,7 @@ export default function CustomConversionButton({ id }) { // The id and group have to be deleted because a new custom query is going to be made. Name is modified. delete convertedQuery.id delete convertedQuery.queryGroupId - convertedQuery.name = `(Derived from) ${convertedQuery.name}` + convertedQuery.name = `(Cloned from) ${convertedQuery.name}` // Generate the search parameters so that we can create a custom query. const searchParams = handleSearchParams(convertedQuery) @@ -146,7 +146,7 @@ export default function CustomConversionButton({ id }) { startIcon={<IconProvider.SettingsSuggestIcon />} sx={{ marginTop: '10px' , marginBottom: '10px'}} > - Customize + Clone as custom query </Button> </Box> ) diff --git a/src/components/CustomQueryEditor/customQueryEditButton.jsx b/src/components/CustomQueryEditor/customQueryEditButton.jsx index e9950b38..d94c2501 100644 --- a/src/components/CustomQueryEditor/customQueryEditButton.jsx +++ b/src/components/CustomQueryEditor/customQueryEditButton.jsx @@ -12,7 +12,7 @@ import configManager from '../../configManager/configManager'; import TextField from '@mui/material/TextField'; -export default function CustomQueryEditButton({ queryID, submitted=false }) { +export default function CustomQueryEditButton({ queryID, submitted = false }) { const customQuery = configManager.getQueryById(queryID); const navigate = useNavigate(); @@ -43,6 +43,16 @@ export default function CustomQueryEditButton({ queryID, submitted=false }) { setCopyUrl(savedUrl); } + const handleDuplication = () => { + + const parameterDuplicate = new URLSearchParams(customQuery.searchParams); + + const newName = "(Cloned) " + parameterDuplicate.get('name') + parameterDuplicate.set('name', newName) + + navigate(`/customQuery?${parameterDuplicate.toString()}`) + } + const handleSaveClose = () => { setSaveOpen(false) setFeedback('') @@ -61,32 +71,45 @@ export default function CustomQueryEditButton({ queryID, submitted=false }) { return ( <React.Fragment> <Box display="flex" justifyContent="flex-end" width={submitted ? '80%' : '100%'} > - - <Button variant="outlined" startIcon={<IconProvider.ModeEditIcon />} onClick={ - () => { - handleEditClick() - }} - sx={{ margin: '10px' }}> - Edit Query - </Button> - - <Button variant="outlined" color="success" startIcon={<IconProvider.SaveIcon />} onClick={ - () => { - handleSave() - setSaveOpen(true) - }} - sx={{ margin: '10px' }}> - Save Query Link - </Button> - <Button variant="outlined" color="error" startIcon={<IconProvider.DeleteIcon />} onClick={ - () => { - setDeleteOpen(true) - }} - sx={{ margin: '10px' }}> - Delete Query - </Button> - + <Button variant="outlined" startIcon={<IconProvider.ModeEditIcon />} onClick={ + () => { + handleEditClick() + }} + sx={{ margin: '10px' }}> + Edit Query + </Button> + + <Button variant="outlined" color="success" startIcon={<IconProvider.SaveIcon />} onClick={ + () => { + handleSave() + setSaveOpen(true) + }} + sx={{ margin: '10px' }}> + Save Query Link + </Button> + + <Button + variant="outlined" + color="warning" + onClick={ + () => { handleDuplication() } + } + type="button" + startIcon={<IconProvider.FilterNoneIcon />} + sx={{ margin: '10px'}} + > + Clone + </Button> + + <Button variant="outlined" color="error" startIcon={<IconProvider.DeleteIcon />} onClick={ + () => { + setDeleteOpen(true) + }} + sx={{ margin: '10px' }}> + Delete Query + </Button> + </Box> <Dialog From 7fe10fc91b146eb87ad2f60902b4064c000bd3e4 Mon Sep 17 00:00:00 2001 From: EmilioTR <Emilio.TenaRomero@hotmail.com> Date: Mon, 19 Aug 2024 14:46:17 +0200 Subject: [PATCH 07/11] updated changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a26c908..c290a78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- The possibility to create a custom query from an existing query (#141). +- The possibility to clone custom queries. + +### Changed + +### Fixed + + ## [1.3.0] - 2024-08-07 ### Added From afc90b7f51367fb2d893b73c73b45ccabec74751 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant <martin.vanbrabant@ugent.be> Date: Mon, 19 Aug 2024 16:40:17 +0200 Subject: [PATCH 08/11] Extended custom queries doc for cloning --- README.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 113c196d..92968596 100644 --- a/README.md +++ b/README.md @@ -224,16 +224,25 @@ Nevertheless, you can use any React component you want, just make sure it's a fu ## Custom queries -Besides the prepared queries in the configuration file, a user can edit custom queries: - -- To create a custom query, open "Custom Query Editor" from the menu on the left. -- Complete the custom query editor form and click the "CREATE QUERY" button when ready. -- Your new query is added to the "Custom queries" group and you are redirected to the query's result view. -- If not satisfied with the query result, you can click "EDIT QUERY" to further edit your query. - When saving changes, the result is recalculated. -- Because the custom query only lives as long as your browser remembers it, a "SAVE QUERY LINK" button is provided. - Use it to generate a unique URL for this custom query. Copy that URL to your clipboard and save it. - You can then visit that URL any time later, to recreate this query. +Besides the prepared queries in the configuration file, a user can create and edit custom queries, either from scratch or based on an existing query. + +- To create a new custom query from scratch: + - Open "Custom Query Editor" from the menu on the left. + - Complete the custom query editor form and click the "CREATE QUERY" button when ready. + - Your new query is added to the "Custom queries" group and you are redirected to the query's result view. + - If not satisfied with the query result, you can click "EDIT QUERY" to further edit your query. + When saving changes, the result is recalculated. + +- To create a new custom query based on an existing query: + - Open the existing query. + - Click "CLONE AS CUSTOM QUERY" (in a normal query) or "CLONE" (in a custom query). + - Make the desired changes in the form and click the "CREATE QUERY" button when ready. The new custom query behaves as if it were created from scratch. + +- To reproduce a custom query later, a "SAVE QUERY LINK" button is provided. + Use it to generate a unique URL for this custom query. + Visiting that URL any time later, recreates a custom query with the same specifications. + This may be useful to forward a custom query to another user. + - To clean up an unwanted custom query, there is always a button "DELETE QUERY"... ## Representation Mapper From 7e705b1cfe85c8f8fbcb5a62722bdafa2ebd85a8 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant <martin.vanbrabant@ugent.be> Date: Mon, 19 Aug 2024 16:43:51 +0200 Subject: [PATCH 09/11] Custom queries doc, part 2 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 92968596..01f80ab5 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,8 @@ Nevertheless, you can use any React component you want, just make sure it's a fu ## Custom queries -Besides the prepared queries in the configuration file, a user can create and edit custom queries, either from scratch or based on an existing query. +The configuration file contains prepared, fixed queries. +In addition, a user can create and edit custom queries, either from scratch or based on an existing query. - To create a new custom query from scratch: - Open "Custom Query Editor" from the menu on the left. From 2f218a96e7b344922c2c70432a9b7a0c1125edbc Mon Sep 17 00:00:00 2001 From: EmilioTR <Emilio.TenaRomero@hotmail.com> Date: Mon, 19 Aug 2024 16:55:46 +0200 Subject: [PATCH 10/11] update --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c290a78b..b380e3e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- The possibility to create a custom query from an existing query (#141). -- The possibility to clone custom queries. +- The possibility to create a custom query from an existing query and also made it possible to clone custom queries (#141). ### Changed From 40134139d277b8b0bd1aef24c71dcd13e09061f8 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant <martin.vanbrabant@ugent.be> Date: Mon, 19 Aug 2024 17:02:28 +0200 Subject: [PATCH 11/11] sentence --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b380e3e6..6fbacb38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- The possibility to create a custom query from an existing query and also made it possible to clone custom queries (#141). +- The possibility to create a custom query from an existing query and to clone a custom query (#141). ### Changed