diff --git a/dev-page/form-configs/sapa/query.json b/dev-page/form-configs/sapa/query.json index 16c6b830..3f15dfd0 100644 --- a/dev-page/form-configs/sapa/query.json +++ b/dev-page/form-configs/sapa/query.json @@ -52,7 +52,7 @@ "line": { "s": "File", "p": "http://shapes.performing-arts.ch/instantiations/File_originalName", - "o": "Z_Text_2", + "o": "OriginalName", "sType": "http://shapes.performing-arts.ch/instantiations/File", "oType": "http://special/Z_Text", "values": [] diff --git a/dev-page/form-page.html b/dev-page/form-page.html index c3ee87a7..faa76784 100644 --- a/dev-page/form-page.html +++ b/dev-page/form-page.html @@ -129,6 +129,6 @@ <%= js %> - + diff --git a/dev-page/scripts/initPage.js b/dev-page/scripts/init-form-page.js similarity index 84% rename from dev-page/scripts/initPage.js rename to dev-page/scripts/init-form-page.js index d1b9a392..ebcd035e 100644 --- a/dev-page/scripts/initPage.js +++ b/dev-page/scripts/init-form-page.js @@ -8,13 +8,7 @@ console.log("urlParams", urlParams); const lang = urlParams.get("lang"); sparnaturalForm.addEventListener("init", (event) => { - console.log("init sparnatural..."); - sparnaturalForm.configuration = { - headers: { "User-Agent": "This is Sparnatural calling" }, - autocomplete: { - maxItems: 40, - }, - }; + console.log("init sparnatural-form..."); console.log("Configuration ", sparnaturalForm.configuration); // Notify all plugins of configuration updates if they support it for (const plugin in yasr.plugins) { @@ -23,27 +17,24 @@ sparnaturalForm.addEventListener("init", (event) => { yasr.plugins[plugin].notifyConfiguration( sparnaturalForm.sparnaturalForm.specProvider ); - console.log("sparnatural", sparnaturalForm.sparnaturalForm.specProvider); } } }); // Listen for updates to the query and pass to YASQE sparnaturalForm.addEventListener("queryUpdated", (event) => { - const queryStringFromJson = sparnaturalForm.expandSparql( - event.detail.queryStringFromJson + const queryString = sparnaturalForm.expandSparql( + event.detail.queryString ); - console.log("queryStringFromJson", event.detail); + console.log("queryString", event.detail); // Update YASQE with the new SPARQL query - yasqe.setValue(queryStringFromJson); - console.log("yasr plugins", yasr.plugins); + yasqe.setValue(queryString); for (const plugin in yasr.plugins) { if (yasr.plugins[plugin].notifyQuery) { yasr.plugins[plugin].notifyQuery(event.detail.queryJson); console.log("notifying query for plugin " + plugin); - console.log(true); } } }); @@ -51,7 +42,19 @@ sparnaturalForm.addEventListener("queryUpdated", (event) => { // Listen for form submission and trigger YASQE query sparnaturalForm.addEventListener("submit", () => { sparnaturalForm.disablePlayBtn(); - yasqe.query(); + let finalResult = sparnaturalForm.executeSparql( + yasqe.getValue(), + (finalResult) => { + // send final result to YasR + yasr.setResponse(finalResult); + // re-enable submit button + sparnaturalForm.enablePlayBtn(); + }, + (error) => { + console.error("Got an error when executing SPARQL in Sparnatural"); + console.dir(error); + } + ); }); console.log("init yasr & yasqe..."); diff --git a/docs/sparnatural-query-schema-openai-dbpedia.json b/docs/sparnatural-query-schema-openai-dbpedia.json new file mode 100644 index 00000000..defd0816 --- /dev/null +++ b/docs/sparnatural-query-schema-openai-dbpedia.json @@ -0,0 +1,351 @@ +{ + "title": "Sparnatural query schema", + "version": "10.0.0", + "description": "A JSON schema describing the structure of queries as read or written by the Sparnatural query builder UI", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://sparnatural.eu/def/schemas/sparnatural-query-schema.json", + "$defs": { + + "VariableTerm": { + "description": "A SPARQL variable with its name, such as '?x'", + "type":"object", + "properties": { + "termType": { + "type" : "string", + "const": "Variable" + }, + "value": { + "type": "string" + } + }, + "required":["termType", "value"], + "additionalProperties":false + }, + + "VariableExpression": { + "description": "A SPARQL aggregation expression, with the aggregation function on the original variable and the resulting variable, such as '(COUNT(?x) AS ?count)' ", + "type":"object", + "properties": { + "variable" : { + "$ref": "#/$defs/VariableTerm" + }, + "expression": { + "description": "An aggregation expression, with an aggregation function and the variable being aggregated, such as 'COUNT(?x)'", + "type":"object", + "properties": { + "type": { + "type":"string", + "const":"aggregate" + }, + "aggregation": { + "description": "The aggregation function name, as one of the allowed SPARQL aggregation functions", + "type":"string", + "enum": ["count","max","min","sample","sum","avg","group_concat"] + }, + "distinct": { + "description": "Whether the aggregation function should use the DISTINCT keywork", + "type":["boolean","null"] + }, + "expression": { + "$ref": "#/$defs/VariableTerm" + } + }, + "required":["type","aggregation","expression","distinct"], + "additionalProperties":false + } + }, + "required":["variable","expression"], + "additionalProperties":false + }, + + "Branch": { + "description": "A Sparnatural query branch, consisting of a criteria line (subject, predicate, object), and optional children branches", + "type" : "object", + "properties": { + "line" : { + "description": "One line of criteria in Sparnatural, corresponding to a subject, a predicate, an object, and also the type (rdf:type) of the subject and the object", + "type":"object", + "properties": { + "s" : { + "description": "The subject variable name, such as '?Person_1'", + "type": "string" + }, + "p" : { + "description": "The full URI of the predicate", + "type": "string", + "enum" : [ +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork_label", +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork_author", +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork_creationYear", +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork_displayedAt", +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork_thumbnail", +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork_description", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum_country", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum_label", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum_numberOfVisitors", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum_displays", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum_inWikidata", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum_thumbnail", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum_description", +"https://data.mydomain.com/ontologies/sparnatural-config/Country_countryOf", +"https://data.mydomain.com/ontologies/sparnatural-config/Country_label", +"https://data.mydomain.com/ontologies/sparnatural-config/Country_deathPlace", +"https://data.mydomain.com/ontologies/sparnatural-config/Country_birthPlace", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_bornIn", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_diedIn", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_label", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_birthDate", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_classifiedIn", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_created", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_deathYear", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_movement", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_thumbnail", +"https://data.mydomain.com/ontologies/sparnatural-config/Person_description", +"https://data.mydomain.com/ontologies/sparnatural-config/Movement_movementIncludes", +"https://data.mydomain.com/ontologies/sparnatural-config/Movement_description", +"https://data.mydomain.com/ontologies/sparnatural-config/Movement_label", +"https://data.mydomain.com/ontologies/sparnatural-config/MuseumWikidata_situe_a" + ] + }, + "o" : { + "description": "The object variable name, such as '?Country_2'", + "type": "string" + }, + "sType" : { + "description": "The full URI of the type of the subject", + "type": "string", + "enum" : [ +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum", +"https://data.mydomain.com/ontologies/sparnatural-config/Country", +"https://data.mydomain.com/ontologies/sparnatural-config/Person", +"https://data.mydomain.com/ontologies/sparnatural-config/Movement", +"https://data.mydomain.com/ontologies/sparnatural-config/Category", +"https://data.mydomain.com/ontologies/sparnatural-config/MuseumWikidata", +"https://data.mydomain.com/ontologies/sparnatural-config/Image", +"https://data.mydomain.com/ontologies/sparnatural-config/Date", +"https://data.mydomain.com/ontologies/sparnatural-config/Position", +"https://data.mydomain.com/ontologies/sparnatural-config/Text", +"https://data.mydomain.com/ontologies/sparnatural-config/Number" + ] + }, + "oType" : { + "description": "The full URI of the type of the object", + "type": "string", + "enum" : [ +"https://data.mydomain.com/ontologies/sparnatural-config/Artwork", +"https://data.mydomain.com/ontologies/sparnatural-config/Museum", +"https://data.mydomain.com/ontologies/sparnatural-config/Country", +"https://data.mydomain.com/ontologies/sparnatural-config/Person", +"https://data.mydomain.com/ontologies/sparnatural-config/Movement", +"https://data.mydomain.com/ontologies/sparnatural-config/Category", +"https://data.mydomain.com/ontologies/sparnatural-config/MuseumWikidata", +"https://data.mydomain.com/ontologies/sparnatural-config/Image", +"https://data.mydomain.com/ontologies/sparnatural-config/Date", +"https://data.mydomain.com/ontologies/sparnatural-config/Position", +"https://data.mydomain.com/ontologies/sparnatural-config/Text", +"https://data.mydomain.com/ontologies/sparnatural-config/Number" + ] + }, + "values": { + "description": "The values selected as search criteria in this line. This is optional, no values may be selected.", + "type": ["array", "null"], + "items": { + "anyOf" : [ + { + "description": "An RDF term value, either an IRI or a Literal", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "type" : { + "description": "The type of the RDF node, either literal, blank node or URI", + "type" : "string", + "enum" : ["literal","bnode","uri"] + }, + "value" : { + "description": "The value, either the URI itself, the lexical form of the literal, or the blank node identifier", + "type" : "string" + }, + "xml:lang" : { + "description": "The language code of the value, if the term is a literal with a language.", + "type" : ["string","null"] + }, + "datatype" : { + "description": "The full URI of the datatype of the Literal, it it has one.", + "type" : ["string","null"] + }, + "label" : { + "description": "The label of the named entity found, with a fake URI", + "type" : ["string","null"] + } + }, + "required" : ["type", "value", "xml:lang", "datatype", "label"], + "additionalProperties": false + }, + { + "description": "A boolean value criteria", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "boolean": { + "description": "The boolean value, either tru or false", + "type":"boolean" + } + }, + "required" : ["boolean"], + "additionalProperties": false + }, + { + "description": "A date range criteria, with a start date and an end date. At least a start date or an end date must be provided.", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "start": { + "description": "The start date of the range, either in a date format, a year format, or a date-time format", + "type":["string","null"] + }, + "stop": { + "description": "The end date of the range, either in a date format, a year format, or a date-time format", + "type":["string","null"] + } + }, + "required":["start", "stop"], + "additionalProperties": false + }, + { + "description": "A GeoSPARQL query criteria. This is a list of shapes, either polygons or rectangles, each polygons containing a list of latitude and longitude pairs", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "type": { + "description": "The type of the searched shape, either 'Rectangle' or 'Polygon'.", + "type": "string", + "enum": ["Rectangle","Polygon"] + }, + "coordinates": { + "description": "The coordinates of the shapes, a list of latitude/longitude pairs", + "type" : "array", + "items" : { + "type" : "array", + "items" : { + "type": "object", + "properties": { + "lat": { + "description": "A latitude", + "type":"number" + }, + "long": { + "description": "A longitude", + "type":"number" + } + }, + "required":["lat","long"], + "additionalProperties": false + } + } + } + }, + "required":["type","coordinates"], + "additionalProperties": false + }, + { + "description": "A number search range, with a minimum and maximum value. At least the minimum or maximum should be provided.", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "min" : { + "type":["number","null"] + }, + "max" : { + "type":["number","null"] + } + }, + "required":["min","max"], + "additionalProperties": false + }, + { + "description": "A string search criteria that will typically be translated to a FILTER(REGEX(...)) SPARQL function", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "regex": { + "description": "The string to search on, that can be a regular expression", + "type":"string" + } + }, + "required":["regex"], + "additionalProperties": false + } + ] + } + } + }, + "required": ["s","p","o","sType","oType", "values"], + "additionalProperties": false + }, + "children": { + "description": "The children branches. This is a recusrsive reference. The array may be empty if there are no children.", + "type":"array", + "items": { + "$ref": "#/$defs/Branch" + } + }, + "optional": { + "description": "Whether the OPTIONAL SPARQL keyword is applied at this level, on this branch and all children branches", + "type":["boolean","null"] + }, + "notExists": { + "description": "Whether the FILTER NOT EXISTS SPARQL keyword is applied at this level, on this branch and all children branches", + "type":["boolean","null"] + } + }, + "required":["line","children","optional","notExists"], + "additionalProperties":false + }, + + "WidgetValue": { + "description": "An abstract selected value. Every value has a human-readable display label.", + "type":"object", + "properties": { + "label" : { + "description": "The human-readable display label of the value", + "type": "string" + } + }, + "required" : ["label"], + "additionalProperties": false + } + }, + "type": "object", + "description": "A full Sparnatural query, with selected variables, query criterias, order clause", + "properties": { + "distinct": { + "description": "Whether the SELECT clause will use a DISTINCT keyword.", + "type":["boolean","null"] + }, + "variables": { + "description": "The variables in the SELECT clause of the query, either simple variables, or aggregated variables", + "type":"array", + "items": { + "anyOf": [ + {"$ref": "#/$defs/VariableTerm"}, + {"$ref": "#/$defs/VariableExpression"} + ] + } + }, + "order": { + "description": "The order criteria to be applied to the first column of the query, either ascending, descending, or no order", + "type":["string", "null"], + "enum": ["asc","desc","noord"] + }, + "branches": { + "description": "The query criterias, forming the 'WHERE' clause of the query", + "type":"array", + "items": { + "$ref": "#/$defs/Branch" + } + } + }, + "required":["distinct","branches","order","variables"], + "additionalProperties":false +} \ No newline at end of file diff --git a/docs/sparnatural-query-schema-openai-template.json b/docs/sparnatural-query-schema-openai-template.json new file mode 100644 index 00000000..cad3d1e4 --- /dev/null +++ b/docs/sparnatural-query-schema-openai-template.json @@ -0,0 +1,286 @@ +{ + "title": "Sparnatural query schema", + "version": "10.0.0", + "description": "A JSON schema describing the structure of queries as read or written by the Sparnatural query builder UI", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://sparnatural.eu/def/schemas/sparnatural-query-schema.json", + "$defs": { + + "VariableTerm": { + "description": "A SPARQL variable with its name, such as '?x'", + "type":"object", + "properties": { + "termType": { + "type" : "string", + "const": "Variable" + }, + "value": { + "type": "string" + } + }, + "required":["termType", "value"], + "additionalProperties":false + }, + + "VariableExpression": { + "description": "A SPARQL aggregation expression, with the aggregation function on the original variable and the resulting variable, such as '(COUNT(?x) AS ?count)' ", + "type":"object", + "properties": { + "variable" : { + "$ref": "#/$defs/VariableTerm" + }, + "expression": { + "description": "An aggregation expression, with an aggregation function and the variable being aggregated, such as 'COUNT(?x)'", + "type":"object", + "properties": { + "type": { + "type":"string", + "const":"aggregate" + }, + "aggregation": { + "description": "The aggregation function name, as one of the allowed SPARQL aggregation functions", + "type":"string", + "enum": ["count","max","min","sample","sum","avg","group_concat"] + }, + "distinct": { + "description": "Whether the aggregation function should use the DISTINCT keywork", + "type":["boolean","null"] + }, + "expression": { + "$ref": "#/$defs/VariableTerm" + } + }, + "required":["type","aggregation","expression","distinct"], + "additionalProperties":false + } + }, + "required":["variable","expression"], + "additionalProperties":false + }, + + "Branch": { + "description": "A Sparnatural query branch, consisting of a criteria line (subject, predicate, object), and optional children branches", + "type" : "object", + "properties": { + "line" : { + "description": "One line of criteria in Sparnatural, corresponding to a subject, a predicate, an object, and also the type (rdf:type) of the subject and the object", + "type":"object", + "properties": { + "s" : { + "description": "The subject variable name, such as '?Person_1'", + "type": "string" + }, + "p" : { + "description": "The full URI of the predicate", + "type": "string" + }, + "o" : { + "description": "The object variable name, such as '?Country_2'", + "type": "string" + }, + "sType" : { + "description": "The full URI of the type of the subject", + "type": "string" + }, + "oType" : { + "description": "The full URI of the type of the object", + "type": "string" + }, + "values": { + "description": "The values selected as search criteria in this line. This is optional, no values may be selected.", + "type": ["array", "null"], + "items": { + "anyOf" : [ + { + "description": "An RDF term value, either an IRI or a Literal", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "type" : { + "description": "The type of the RDF node, either literal, blank node or URI", + "type" : "string", + "enum" : ["literal","bnode","uri"] + }, + "value" : { + "description": "The value, either the URI itself, the lexical form of the literal, or the blank node identifier", + "type" : "string" + }, + "xml:lang" : { + "description": "The language code of the value, if the term is a literal with a language.", + "type" : ["string","null"] + }, + "datatype" : { + "description": "The full URI of the datatype of the Literal, it it has one.", + "type" : ["string","null"] + } + }, + "required" : ["type", "value", "xml:lang", "datatype"], + "additionalProperties": false + }, + { + "description": "A boolean value criteria", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "boolean": { + "description": "The boolean value, either tru or false", + "type":"boolean" + } + }, + "required" : ["boolean"], + "additionalProperties": false + }, + { + "description": "A date range criteria, with a start date and an end date. At least a start date or an end date must be provided.", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "start": { + "description": "The start date of the range, either in a date format, a year format, or a date-time format", + "type":["string","null"] + }, + "stop": { + "description": "The end date of the range, either in a date format, a year format, or a date-time format", + "type":["string","null"] + } + }, + "required":["start", "stop"], + "additionalProperties": false + }, + { + "description": "A GeoSPARQL query criteria. This is a list of shapes, either polygons or rectangles, each polygons containing a list of latitude and longitude pairs", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "type": { + "description": "The type of the searched shape, either 'Rectangle' or 'Polygon'.", + "type": "string", + "enum": ["Rectangle","Polygon"] + }, + "coordinates": { + "description": "The coordinates of the shapes, a list of latitude/longitude pairs", + "type" : "array", + "items" : { + "type" : "array", + "items" : { + "type": "object", + "properties": { + "lat": { + "description": "A latitude", + "type":"number" + }, + "long": { + "description": "A longitude", + "type":"number" + } + }, + "required":["lat","long"], + "additionalProperties": false + } + } + } + }, + "required":["type","coordinates"], + "additionalProperties": false + }, + { + "description": "A number search range, with a minimum and maximum value. At least the minimum or maximum should be provided.", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "min" : { + "type":["number","null"] + }, + "max" : { + "type":["number","null"] + } + }, + "required":["min","max"], + "additionalProperties": false + }, + { + "description": "A string search criteria that will typically be translated to a FILTER(REGEX(...)) SPARQL function", + "type":"object", + "extends" : [{"$ref": "#/$defs/WidgetValue"}], + "properties": { + "regex": { + "description": "The string to search on, that can be a regular expression", + "type":"string" + } + }, + "required":["regex"], + "additionalProperties": false + } + ] + } + } + }, + "required": ["s","p","o","sType","oType", "values"], + "additionalProperties": false + }, + "children": { + "description": "The children branches. This is a recusrsive reference. The array may be empty if there are no children.", + "type":"array", + "items": { + "$ref": "#/$defs/Branch" + } + }, + "optional": { + "description": "Whether the OPTIONAL SPARQL keyword is applied at this level, on this branch and all children branches", + "type":["boolean","null"] + }, + "notExists": { + "description": "Whether the FILTER NOT EXISTS SPARQL keyword is applied at this level, on this branch and all children branches", + "type":["boolean","null"] + } + }, + "required":["line","children","optional","notExists"], + "additionalProperties":false + }, + + "WidgetValue": { + "description": "An abstract selected value. Every value has a human-readable display label.", + "type":"object", + "properties": { + "label" : { + "description": "The human-readable display label of the value", + "type": "string" + } + }, + "required" : ["label"], + "additionalProperties": false + } + }, + "type": "object", + "description": "A full Sparnatural query, with selected variables, query criterias, order clause", + "properties": { + "branches": { + "description": "The query criterias, forming the 'WHERE' clause of the query", + "type":"array", + "items": { + "$ref": "#/$defs/Branch" + } + }, + "distinct": { + "description": "Whether the SELECT clause will use a DISTINCT keyword.", + "type":["boolean","null"] + }, + "order": { + "description": "The order criteria to be applied to the first column of the query, either ascending, descending, or no order", + "type":["string", "null"], + "enum": ["asc","desc","noord"] + }, + "variables": { + "description": "The variables in the SELECT clause of the query, either simple variables, or aggregated variables", + "type":"array", + "items": { + "anyOf": [ + {"$ref": "#/$defs/VariableTerm"}, + {"$ref": "#/$defs/VariableExpression"} + ] + } + } + }, + "required":["distinct","branches","order","variables"], + "additionalProperties":false +} \ No newline at end of file diff --git a/package.json b/package.json index 9ba0128f..d23a1490 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sparnatural", - "version": "10.0.0", + "version": "10.1.0", "description": "Visual client-side SPARQL query builder and knowledge graph exploration tool", "main": "dist/sparnatural.js", "module": "dist/sparnatural.js", diff --git a/src/SparnaturalAttributes.ts b/src/SparnaturalAttributes.ts index 9f78b9d6..a4ddd1e1 100644 --- a/src/SparnaturalAttributes.ts +++ b/src/SparnaturalAttributes.ts @@ -31,7 +31,7 @@ export class SparnaturalAttributes { throw Error("No endpoint provided!"); } this.catalog = this.#read(element, "catalog"); - //this.language = this.#read(element, "lang"); + this.language = this.#read(element, "lang"); this.defaultLanguage = this.#read(element, "defaultLang"); // use the singular to match RDFa attribute name @@ -55,14 +55,14 @@ export class SparnaturalAttributes { } #parsePrefixes(element: HTMLElement) { - if (!element.getAttribute("prefix")) { + if (!element.getAttribute("prefixes")) { return; } let sparqlPrefixes = {}; // use the singular to match RDFa attribute name let prefixArray = element - .getAttribute("prefix") + .getAttribute("prefixes") .trim() .split(/:\s+|\s+/); for (let i = 0; i < prefixArray.length; i++) { @@ -77,11 +77,10 @@ export class SparnaturalAttributes { enumerable: true, }); } catch (e) { - console.error("Parsing of attribute prefix failed!"); + console.error("Parsing of attribute prefixes failed!"); console.error(`Can not parse ${prefixArray[i]}`); } } - console.log("Prefixes at Attributes", sparqlPrefixes); return sparqlPrefixes; } diff --git a/src/SparnaturalFormElement.ts b/src/SparnaturalFormElement.ts index bca1b3ca..43c97fbf 100644 --- a/src/SparnaturalFormElement.ts +++ b/src/SparnaturalFormElement.ts @@ -1,8 +1,7 @@ import $ from "jquery"; -import "./assets/stylesheets/SparnaturalForm.scss"; +import "./assets/stylesheets/sparnatural-form.scss"; import SparnaturalFormComponent from "./sparnatural-form/components/SparnaturalFormComponent"; import { SparnaturalFormAttributes } from "./SparnaturalFormAttributes"; -import SparnaturalComponent from "./sparnatural/components/SparnaturalComponent"; import { getSettings, mergeSettings, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index dcda81c7..4384f217 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -23,6 +23,7 @@ "DisplayValueDateFrom": "From", "DisplayValueDateTo": "Until", "ButtonAdd": "Add", + "ListWidgetSelectValue": "Select a value...", "ListWidgetNoItem": "... Oups, no value found !", "StartClassTemporaryLabel": "Search for resources", "ObjectPropertyTemporaryLabel": "related to", diff --git a/src/assets/lang/fr.json b/src/assets/lang/fr.json index 19e40984..ebe1b3cc 100644 --- a/src/assets/lang/fr.json +++ b/src/assets/lang/fr.json @@ -23,6 +23,7 @@ "DisplayValueDateFrom": "A partir de", "DisplayValueDateTo": "Jusqu'à", "ButtonAdd": "Ajouter", + "ListWidgetSelectValue": "Selectionnez une valeur...", "ListWidgetNoItem": "... Oups, aucune valeur trouvée !", "StartClassTemporaryLabel": "Chercher des ressources", "ObjectPropertyTemporaryLabel": "relié(e) à", diff --git a/src/assets/stylesheets/SparnaturalForm.scss b/src/assets/stylesheets/SparnaturalForm.scss deleted file mode 100644 index f657b94b..00000000 --- a/src/assets/stylesheets/SparnaturalForm.scss +++ /dev/null @@ -1,10 +0,0 @@ -@use './base'; -// we are importing one default theme, -// so that all the variables have a default value. -// they can be overriden by importing another theme CSS in the HTML page -@import "themes/sparnatural-theme-grass.css"; - -sparnatural-form { - - -} \ No newline at end of file diff --git a/src/assets/stylesheets/components/all-components-sparnatural-form.scss b/src/assets/stylesheets/components/all-components-sparnatural-form.scss new file mode 100644 index 00000000..5265ee7e --- /dev/null +++ b/src/assets/stylesheets/components/all-components-sparnatural-form.scss @@ -0,0 +1,20 @@ +//Buttons +@use './buttons/unselectbtn.scss'; +@use './buttons/optionalarrow.scss'; +@use './buttons/resetbtn.scss'; +@use './buttons/selectviewvariablebtn.scss'; +@use './buttons/AddUserInputBtn.scss'; +@use './buttons/InfoBtn.scss'; +@use './buttons/AddMoreValuesBtn.scss'; +@use './buttons/editBtn.scss'; +//Widget +@use './widget/EndClassWidgetGroup.scss'; +@use './widget/EndClassWidgetValue.scss'; +@use './widget/WidgetWrapper.scss'; +@use './widget/AutocompleteWidget.scss'; +@use './widget/ListWidget.scss'; +@use './widget/MapWidget.scss'; +@use './widget/TreeWidget.scss'; +@use './widget/Widgets.scss'; +// LoadingSpinner +@use './widget/LoadingSpinner.scss'; diff --git a/src/assets/stylesheets/sparnatural-form.scss b/src/assets/stylesheets/sparnatural-form.scss new file mode 100644 index 00000000..ab4fd6bd --- /dev/null +++ b/src/assets/stylesheets/sparnatural-form.scss @@ -0,0 +1,121 @@ +@use './base'; +// Importing all the components +@use './components/all-components-sparnatural-form.scss'; +// we are importing one default theme, +// so that all the variables have a default value. +// they can be overriden by importing another theme CSS in the HTML page +@import "themes/sparnatural-theme-grass.css"; + +sparnatural-form { + display: block; + padding: 20px; + /* This is the same color as background of Sparnatural with default theme "grass" */ + background-color: #e8fcf5; + border-radius: 5px; + margin-bottom: 20px; + + .formField { + margin-bottom: 10px; + } + + /* Display container for selected values */ + .value-display-container { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; + } + + /* Each selected value container */ + .selected-value-container { + display: flex; + align-items: center; + padding: 5px 10px; + background-color: var(--primary-color-medium); + /* border: 1px solid #4db6ac; */ + border-radius: 5px; + font-size: 13px; + margin-bottom: 5px; + } + + /* Align selected values horizontally with wrapping */ + .selected-value-container { + flex-direction: row; + /* border: 1px solid #ccc; */ + padding: 5px; + display: inline-block; + margin-right: 10px; + margin-bottom: 10px; + } + + /* Styling the selected value label */ + .selected-value-label { + margin-right: 10px; + font-weight: normal; + color: var(--default-text-color); + } + + /* Container for the search button to control its position */ + .button-container { + display: flex; + justify-content: flex-end; + margin-top: 20px; + } + + .hr { + color: inherit; + border: 0; + border-top-width: 0px; + border-top-style: none; + border-top-color: currentcolor; + border-top: 1px solid; + opacity: 0.25; + } + + #submit { + display: flex; + justify-content: flex-end; + gap: 10px; + + button { + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + } + + button[id="Search"] { + background-color: var(--secondary-color-important); + color: #000; + border: 1px solid #ddd; + box-shadow: 0px 2px 3px 0px #cfcfcf; + } + + button[id="Reset"] { + background-color: #fff; + color: #000; + border: 1px solid #ddd; + box-shadow: 0px 2px 3px 0px #cfcfcf; + } + + } + + .option-container { + display: flex; + flex-wrap: wrap; + gap: 15px; + margin-bottom: 10px; + } + + .any-value-container, + .not-exist-container { + display: flex; + flex-wrap: wrap; + gap: px; + } +} + + + + \ No newline at end of file diff --git a/src/sparnatural-form/FormStructur.ts b/src/sparnatural-form/FormStructure.ts similarity index 100% rename from src/sparnatural-form/FormStructur.ts rename to src/sparnatural-form/FormStructure.ts diff --git a/src/sparnatural-form/components/SparnaturalFormComponent.ts b/src/sparnatural-form/components/SparnaturalFormComponent.ts index f76e3b36..8c176570 100644 --- a/src/sparnatural-form/components/SparnaturalFormComponent.ts +++ b/src/sparnatural-form/components/SparnaturalFormComponent.ts @@ -3,19 +3,15 @@ import HTMLComponent from "../../sparnatural/components/HtmlComponent"; import { ISparJson } from "../../sparnatural/generators/json/ISparJson"; import { I18n } from "../../sparnatural/settings/I18n"; import ISparnaturalSpecification from "../../sparnatural/spec-providers/ISparnaturalSpecification"; -import ISpecificationEntity from "../../sparnatural/spec-providers/ISpecificationEntity"; import SparnaturalSpecificationFactory from "../../sparnatural/spec-providers/SparnaturalSpecificationFactory"; import { SparnaturalFormAttributes } from "../../SparnaturalFormAttributes"; import ISettings from "../settings/ISettings"; import { SparnaturalFormI18n } from "../settings/SparnaturalFormI18n"; import UnselectBtn from "../../sparnatural/components/buttons/UnselectBtn"; -import "../style/form.scss"; -import ActionStoreForm from "../handling/ActionStoreForm"; // Importer le store +import ActionStoreForm from "../handling/ActionStore"; // Importer le store import { Catalog } from "../../sparnatural/settings/Catalog"; import { getSettings } from "../settings/defaultsSettings"; import SubmitSection from "./buttons/SubmitBtn"; -import { value } from "../../../../Sparnatural-yasgui-plugins/src/sparnatural-yasr-plugin-grid/old-backup/Models/value"; -import { ValueBuilderFactory } from "../../sparnatural/generators/sparql/ValueBuilder"; import { SparnaturalFormElement } from "../../SparnaturalFormElement"; class SparnaturalFormComponent extends HTMLComponent { @@ -174,16 +170,21 @@ class SparnaturalFormComponent extends HTMLComponent { formConfig.bindings.forEach((binding: any) => { const variable = binding.variable; + // create a div that contains this form field + const formFieldDiv = document.createElement("div"); + formFieldDiv.classList.add("formField"); + this.html[0].appendChild(formFieldDiv); + // Create a label for the widget and append it to the form const label = document.createElement("label"); label.setAttribute("for", variable); label.innerHTML = `${SparnaturalFormI18n.getLabel( variable - )} :`; + )}`; label.style.fontSize = "18px"; - this.html[0].appendChild(label); + formFieldDiv.appendChild(label); const label1 = label.cloneNode(true); // resumeSection.appendChild(label1); @@ -248,7 +249,7 @@ class SparnaturalFormComponent extends HTMLComponent { { variable: queryLine.o, type: object } ); theWidget.render(); - this.html[0].appendChild(theWidget.html[0]); + formFieldDiv.appendChild(theWidget.html[0]); /**/ const selectedValues = new Set(); @@ -257,7 +258,7 @@ class SparnaturalFormComponent extends HTMLComponent { valueDisplay.setAttribute("id", `selected-value-${variable}`); valueDisplay.classList.add("value-display-container"); valueDisplay.style.marginTop = "5px"; - this.html[0].appendChild(valueDisplay); + formFieldDiv.appendChild(valueDisplay); const updateValueDisplay = () => { valueDisplay.innerHTML = ""; @@ -279,7 +280,7 @@ class SparnaturalFormComponent extends HTMLComponent { updateValueDisplay(); queryLine.values = Array.from(selectedValues); - this.html[0].dispatchEvent( + formFieldDiv.dispatchEvent( new CustomEvent("valueRemoved", { bubbles: true, detail: { value: val, variable: variable }, @@ -325,7 +326,7 @@ class SparnaturalFormComponent extends HTMLComponent { updateOptionVisibility(); // Update options visibility // Dispatch valueAdded event - this.html[0].dispatchEvent( + formFieldDiv.dispatchEvent( new CustomEvent("valueAdded", { bubbles: true, detail: { value: val, variable: variable }, @@ -345,10 +346,35 @@ class SparnaturalFormComponent extends HTMLComponent { optionContainer.classList.add("option-container"); const anydiv = document.createElement("div"); const anyValueToggle = document.createElement("input"); + const notExistDiv = document.createElement("div"); + notExistDiv.classList.add("not-exist-container"); + const notExistToggle = document.createElement("input"); + // Si la variable est optionnelle, on ajoute l'option "Any value" console.log("variable", variable); console.log("branch", branch); console.log("branchparent", branchparent); + + // Fonction de mise à jour de la visibilité des options + const updateOptionVisibility = () => { + const hasValues = + queryLine.values && queryLine.values.length > 0; + + if (hasValues) { + if (anyValueToggle) anyValueToggle.checked = false; + if (notExistToggle) notExistToggle.checked = false; + if (anyValueToggle) anyValueToggle.disabled = true; + if (notExistToggle) notExistToggle.disabled = true; + anydiv.style.display = "none"; + notExistDiv.style.display = "none"; + } else { + if (anyValueToggle) anyValueToggle.disabled = false; + if (notExistToggle) notExistToggle.disabled = false; + anydiv.style.display = "block"; + notExistDiv.style.display = "block"; + } + }; + if (branch.optional || branchparent.optional) { // Création de l'élément "Any value" @@ -359,7 +385,7 @@ class SparnaturalFormComponent extends HTMLComponent { anyValueToggle.id = `any-value-${variable}`; anyValueToggle.classList.add("any-value-toggle"); anyValueLabel.htmlFor = `any-value-${variable}`; - anyValueLabel.innerText = "Any value"; + anyValueLabel.innerHTML = " Any value"; anydiv.appendChild(anyValueToggle); anydiv.appendChild(anyValueLabel); @@ -370,8 +396,8 @@ class SparnaturalFormComponent extends HTMLComponent { if (anyValueToggle.checked) { this.setAnyValueForWidget(variable); notExistDiv.style.display = "none"; // Masquer "Not Exist" - notExistValue.checked = false; - notExistValue.disabled = true; + notExistToggle.checked = false; + notExistToggle.disabled = true; theWidget.disableWidget(); @@ -380,7 +406,7 @@ class SparnaturalFormComponent extends HTMLComponent { "selected-value-container" ); - this.html[0].dispatchEvent( + formFieldDiv.dispatchEvent( new CustomEvent("anyValueSelected", { bubbles: true, detail: { variable: variable }, @@ -389,7 +415,7 @@ class SparnaturalFormComponent extends HTMLComponent { this.cleanQuery(); } else { this.resetToDefaultValueForWidget(variable); - notExistValue.disabled = false; + notExistToggle.disabled = false; notExistDiv.style.display = "block"; // Afficher "Not Exist" theWidget.enableWidget(); @@ -398,7 +424,7 @@ class SparnaturalFormComponent extends HTMLComponent { "selected-value-container" ); - this.html[0].dispatchEvent( + formFieldDiv.dispatchEvent( new CustomEvent("removeAnyValueOption", { bubbles: true, detail: { variable: variable }, @@ -407,76 +433,54 @@ class SparnaturalFormComponent extends HTMLComponent { this.cleanQuery(); } }); - } - // Création de l'élément "Not Exist" - const notExistDiv = document.createElement("div"); - notExistDiv.classList.add("not-exist-container"); - const notExistValue = document.createElement("input"); - const notExistLabel = document.createElement("label"); - notExistValue.type = "checkbox"; - notExistValue.id = `not-value-${variable}`; - notExistValue.classList.add("any-value-toggle"); - notExistLabel.htmlFor = `not-value-${variable}`; - notExistLabel.innerText = "Not Exist"; - notExistDiv.appendChild(notExistValue); - notExistDiv.appendChild(notExistLabel); - - // Ajout de l'option "Not Exist" au conteneur d'options - optionContainer.appendChild(notExistDiv); - this.html[0].appendChild(optionContainer); - - // Mise à jour de la visibilité des options - const updateOptionVisibility = () => { - const hasValues = - queryLine.values && queryLine.values.length > 0; + // Création de l'élément "Not Exist" + const notExistLabel = document.createElement("label"); + notExistToggle.type = "checkbox"; + notExistToggle.id = `not-value-${variable}`; + notExistToggle.classList.add("any-value-toggle"); + notExistLabel.htmlFor = `not-value-${variable}`; + notExistLabel.innerHTML = " Not Exist"; + notExistDiv.appendChild(notExistToggle); + notExistDiv.appendChild(notExistLabel); + + // Ajout de l'option "Not Exist" au conteneur d'options + optionContainer.appendChild(notExistDiv); + formFieldDiv.appendChild(optionContainer); + + updateOptionVisibility(); + + // Gestion des événements pour le bouton "Not Exist" + notExistToggle.addEventListener("change", () => { + if (notExistToggle.checked) { + this.setNotExistsForWidget(variable); + if (anyValueToggle) anyValueToggle.checked = false; + if (anyValueToggle) anyValueToggle.disabled = true; + anydiv.style.display = "none"; // Masquer "Any value" + theWidget.disableWidget(); - if (hasValues) { - if (anyValueToggle) anyValueToggle.checked = false; - notExistValue.checked = false; - if (anyValueToggle) anyValueToggle.disabled = true; - notExistValue.disabled = true; - anydiv.style.display = "none"; - notExistDiv.style.display = "none"; - } else { - if (anyValueToggle) anyValueToggle.disabled = false; - notExistValue.disabled = false; - anydiv.style.display = "block"; - notExistDiv.style.display = "block"; - } - }; + formFieldDiv.dispatchEvent( + new CustomEvent("notExist", { + bubbles: true, + detail: { variable: variable }, + }) + ); + } else { + this.removeNotExistsForWidget(variable); + if (anyValueToggle) anyValueToggle.disabled = false; + anydiv.style.display = "block"; // Afficher "Any value" + theWidget.enableWidget(); - updateOptionVisibility(); + formFieldDiv.dispatchEvent( + new CustomEvent("removeNotExistOption", { + bubbles: true, + detail: { variable: variable }, + }) + ); + } + }); - // Gestion des événements pour le bouton "Not Exist" - notExistValue.addEventListener("change", () => { - if (notExistValue.checked) { - this.setNotExistsForWidget(variable); - if (anyValueToggle) anyValueToggle.checked = false; - if (anyValueToggle) anyValueToggle.disabled = true; - anydiv.style.display = "none"; // Masquer "Any value" - theWidget.disableWidget(); - - this.html[0].dispatchEvent( - new CustomEvent("notExist", { - bubbles: true, - detail: { variable: variable }, - }) - ); - } else { - this.removeNotExistsForWidget(variable); - if (anyValueToggle) anyValueToggle.disabled = false; - anydiv.style.display = "block"; // Afficher "Any value" - theWidget.enableWidget(); - - this.html[0].dispatchEvent( - new CustomEvent("removeNotExistOption", { - bubbles: true, - detail: { variable: variable }, - }) - ); - } - }); + } } }); diff --git a/src/sparnatural-form/handling/ActionStoreForm.ts b/src/sparnatural-form/handling/ActionStore.ts similarity index 95% rename from src/sparnatural-form/handling/ActionStoreForm.ts rename to src/sparnatural-form/handling/ActionStore.ts index d7e06c71..854f5ee1 100644 --- a/src/sparnatural-form/handling/ActionStoreForm.ts +++ b/src/sparnatural-form/handling/ActionStore.ts @@ -1,13 +1,11 @@ import SparnaturalFormComponent from "../components/SparnaturalFormComponent"; import ISparnaturalSpecification from "../../sparnatural/spec-providers/ISparnaturalSpecification"; -import { QueryGeneratorForm } from "./actions/GenerateQueryForm"; +import { QueryGeneratorForm } from "./actions/GenerateQuery"; class ActionStoreForm { sparnaturalForm: SparnaturalFormComponent; specProvider: any; quiet = false; // Pour éviter d'exécuter des actions quand c'est nécessaire de "garder le silence" - language = "en"; //default - sparqlVarID = 0; constructor( sparnaturalForm: SparnaturalFormComponent, diff --git a/src/sparnatural-form/handling/actions/GenerateQueryForm.ts b/src/sparnatural-form/handling/actions/GenerateQuery.ts similarity index 86% rename from src/sparnatural-form/handling/actions/GenerateQueryForm.ts rename to src/sparnatural-form/handling/actions/GenerateQuery.ts index dd54fb68..1c742e54 100644 --- a/src/sparnatural-form/handling/actions/GenerateQueryForm.ts +++ b/src/sparnatural-form/handling/actions/GenerateQuery.ts @@ -1,5 +1,5 @@ import { getSettings } from "../../settings/defaultsSettings"; -import ActionStoreForm from "../ActionStoreForm"; +import ActionStoreForm from "../ActionStore"; import { Generator } from "sparqljs"; import { ISparJson } from "../../../sparnatural/generators/json/ISparJson"; import JsonSparqlTranslator from "../../../sparnatural/generators/sparql/fromjson/JsonSparqlTranslator"; @@ -30,10 +30,6 @@ export class QueryGeneratorForm { // Utiliser la cleanQuery si elle existe, sinon jsonQuery queryToUse = this.actionStoreForm.sparnaturalForm.cleanQueryResult; - let queryJson = this.actionStoreForm.sparnaturalForm.jsonQuery; - - const jsonQuery = this.actionStoreForm.sparnaturalForm.jsonQuery; - console.log("Query utilisée pour la génération :", queryToUse); // Utiliser JsonSparqlTranslator pour convertir la requête en SPARQL @@ -50,9 +46,7 @@ export class QueryGeneratorForm { // Créer un payload avec la requête générée const queryPayload: QueryUpdatedPayload = { queryString: queryStringFromJson, - queryJson: queryToUse, - querySparqlJs: sparqlJsQuery, - queryStringFromJson: queryStringFromJson, + queryJson: queryToUse }; // Déclencher l'événement pour notifier que la requête a été mise à jour @@ -60,8 +54,6 @@ export class QueryGeneratorForm { // re-enable submit button if it was disabled this.actionStoreForm.sparnaturalForm.SubmitSection.enableSubmit(); - - //return queryPayload; } fireQueryUpdatedEvent(payload: QueryUpdatedPayload) { @@ -78,6 +70,4 @@ export class QueryGeneratorForm { export class QueryUpdatedPayload { queryString: string; queryJson: ISparJson; - querySparqlJs: Object; - queryStringFromJson: string; } diff --git a/src/sparnatural-form/style/form.scss b/src/sparnatural-form/style/form.scss deleted file mode 100644 index 2e5c30c3..00000000 --- a/src/sparnatural-form/style/form.scss +++ /dev/null @@ -1,107 +0,0 @@ -/* Display container for selected values */ -.value-display-container { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 10px; -} - -/* Each selected value container */ -.selected-value-container { - display: flex; - align-items: center; - padding: 5px 10px; - background-color: #e0f7fa; - border: 1px solid #4db6ac; - border-radius: 5px; - font-size: 13px; - margin-bottom: 5px; -} - -/* Align selected values horizontally with wrapping */ -.selected-value-container { - flex-direction: row; - border: 1px solid #ccc; - padding: 5px; - display: inline-block; - margin-right: 10px; - margin-bottom: 10px; -} - -/* Styling the selected value label */ -.selected-value-label { - margin-right: 10px; - font-weight: normal; - color: #00796b; -} - -/* Container for the search button to control its position */ -.button-container { - display: flex; - justify-content: flex-end; - margin-top: 20px; -} - -.hr { - color: inherit; - border: 0; - border-top-width: 0px; - border-top-style: none; - border-top-color: currentcolor; - border-top: 1px solid; - opacity: 0.25; -} -sparnatural-form { - display: block; - padding: 20px; - background-color: #f0f8f8; - border-radius: 5px; - margin-bottom: 20px; -} -#submit { - display: flex; - justify-content: flex-end; - gap: 10px; -} - -#submit button { - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 14px; -} - -#submit button[id="Search"] { - background-color: #a5d6a7; - color: #000; -} - -#submit button[id="Search"]:hover { - background-color: #81c784; - color: #fff; -} - -#submit button[id="Reset"] { - background-color: #973d46a2; - color: #000; -} - -#submit button[id="Reset"]:hover { - background-color: #973d46; - color: #fff; -} - -.option-container { - display: flex; - flex-wrap: wrap; - gap: 15px; - margin-bottom: 10px; -} - -.any-value-container, -.not-exist-container { - display: flex; - flex-wrap: wrap; - gap: px; -} diff --git a/src/sparnatural/components/widgets/AutoCompleteWidget.ts b/src/sparnatural/components/widgets/AutoCompleteWidget.ts index 380d06a0..e53639fd 100644 --- a/src/sparnatural/components/widgets/AutoCompleteWidget.ts +++ b/src/sparnatural/components/widgets/AutoCompleteWidget.ts @@ -4,7 +4,7 @@ import { SelectedVal } from "../SelectedVal"; import SparqlFactory from "../../generators/sparql/SparqlFactory"; import { AbstractWidget, RDFTerm, RdfTermValue, ValueRepetition, WidgetValue } from "./AbstractWidget"; import EndClassGroup from "../builder-section/groupwrapper/criteriagroup/startendclassgroup/EndClassGroup"; -import { AutocompleteDataProviderIfc, NoOpAutocompleteProvider } from "./data/DataProviders"; +import { AutocompleteDataProviderIfc, mergeDatasourceResults, NoOpAutocompleteProvider, RdfTermDatasourceItem } from "./data/DataProviders"; import Awesomplete from 'awesomplete'; import { I18n } from '../../settings/I18n'; import HTMLComponent from '../HtmlComponent'; @@ -74,16 +74,33 @@ export class AutoCompleteWidget extends AbstractWidget { // the callback called when proposals have been fetched, to populate the suggestion list - let callback = (items:{term:RDFTerm;label:string;group?:string}[]) => { + let callback = (items:RdfTermDatasourceItem[]) => { + // find distinct values of the 'group' binding + const groups = [...new Set(items.map(item => item.group))]; + let list = new Array<{label:String, value:String}>(); - $.each(items, (key, item) => { - // Awesomplete list will contain the label as 'label', and the RDFTerm JSON serialization as 'value' - list.push({ - label: (item.group)?""+item.label+"":item.label, - value: JSON.stringify(item.term) + if(groups.length == 1 && groups[0] == undefined) { + // no groups defined at all + + items.forEach(item => { + // Awesomplete list will contain the label as 'label', and the RDFTerm JSON serialization as 'value' + list.push({ + label: item.label, + value: JSON.stringify(item.term) + }); }); - }); + } else { + // we have some groups, merge + let mergedResult = mergeDatasourceResults(items); + + mergedResult.forEach(item => { + list.push({ + label: (item.group)?""+item.label+"":item.label, + value: JSON.stringify(item.term) + }); + }); + } // toggle spinner if(list.length == 0) { @@ -152,3 +169,4 @@ export class AutoCompleteWidget extends AbstractWidget { parseInput(input: RdfTermValue["value"]): RdfTermValue {return new RdfTermValue(input)} } + diff --git a/src/sparnatural/components/widgets/ListWidget.ts b/src/sparnatural/components/widgets/ListWidget.ts index 0b79a7e5..effaa67a 100644 --- a/src/sparnatural/components/widgets/ListWidget.ts +++ b/src/sparnatural/components/widgets/ListWidget.ts @@ -6,7 +6,7 @@ import "select2"; import "select2/dist/css/select2.css"; import SparqlFactory from "../../generators/sparql/SparqlFactory"; import EndClassGroup from "../builder-section/groupwrapper/criteriagroup/startendclassgroup/EndClassGroup"; -import { ListDataProviderIfc, RdfTermDatasourceItem, NoOpListDataProvider } from "./data/DataProviders"; +import { ListDataProviderIfc, RdfTermDatasourceItem, NoOpListDataProvider, mergeDatasourceResults } from "./data/DataProviders"; import { I18n } from "../../settings/I18n"; import { Term } from "@rdfjs/types/data-model"; import HTMLComponent from "../HtmlComponent"; @@ -72,7 +72,11 @@ export class ListWidget extends AbstractWidget { let callback = (items:RdfTermDatasourceItem[]) => { if (items.length > 0) { - + + this.selectHtml.append( + $("") + ); + // find distinct values of the 'group' binding const groups = [...new Set(items.map(item => item.group))]; @@ -86,10 +90,14 @@ export class ListWidget extends AbstractWidget { ); }); } else { - // we found some group, organise the list content with optgroup - groups.forEach(group => { + // we found some groups, organise the list content with optgroup + + let mergedResult = mergeDatasourceResults(items); + const groupsAfterMerge = [...new Set(mergedResult.map(item => item.group))]; + + groupsAfterMerge.forEach(group => { let html = ""; - items.filter(item => (item.group == group)).forEach(item => { + mergedResult.filter(item => (item.group == group)).forEach(item => { // select item label : either displayed label, or itemLabel if provided let itemLabel = item.itemLabel?item.itemLabel:item.label; @@ -116,9 +124,13 @@ export class ListWidget extends AbstractWidget { if (option.length > 1) throw Error("List widget should allow only for one el to be selected!"); - let itemLabel = option[0].getAttribute("data-itemLabel"); - let listWidgetValue: WidgetValue = this.buildValue(option[0].value, itemLabel); - this.renderWidgetVal(listWidgetValue); + // this is the placeholder + if(option[0].value == "") + return; + + let itemLabel = option[0].getAttribute("data-itemLabel"); + let listWidgetValue: WidgetValue = this.buildValue(option[0].value, itemLabel); + this.renderWidgetVal(listWidgetValue); }); } else { diff --git a/src/sparnatural/components/widgets/data/DataProviders.ts b/src/sparnatural/components/widgets/data/DataProviders.ts index 66396058..c8aefde1 100644 --- a/src/sparnatural/components/widgets/data/DataProviders.ts +++ b/src/sparnatural/components/widgets/data/DataProviders.ts @@ -116,21 +116,25 @@ export class SparqlListDataProvider implements ListDataProviderIfc { let result = new Array; for (let index = 0; index < data.results.bindings.length; index++) { const solution = data.results.bindings[index]; - if(solution.uri) { - // if we find a "uri" column... - // read uri key & label key - result[result.length] = {term:solution.uri, label:solution.label.value, group:solution.group?.value, itemLabel:solution.itemLabel?.value}; - } else if(solution.value) { - // if we find a "value" column... - // read value key & label key - result[result.length] = {term:solution.value, label:solution.label.value, group:solution.group?.value, itemLabel:solution.itemLabel?.value}; - } else { - // try to determine the payload column by taking the column other than label - let columnName = this.getRdfTermColumn(solution); - if(columnName) { - result[result.length] ={term:solution[columnName], label:solution.label.value, group:solution.group?.value, itemLabel:solution.itemLabel?.value}; + // this is to avoid corner-cases with GraphDB queries returning only count=0 in aggregation queries. + // we need at least 2 bindings anyway + if(Object.keys(solution).length > 1) { + if(solution.uri) { + // if we find a "uri" column... + // read uri key & label key + result[result.length] = {term:solution.uri, label:solution.label.value, group:solution.group?.value, itemLabel:solution.itemLabel?.value}; + } else if(solution.value) { + // if we find a "value" column... + // read value key & label key + result[result.length] = {term:solution.value, label:solution.label.value, group:solution.group?.value, itemLabel:solution.itemLabel?.value}; } else { - throw Error("Could not determine which column to read from the result set") + // try to determine the payload column by taking the column other than label + let columnName = this.getRdfTermColumn(solution); + if(columnName) { + result[result.length] ={term:solution[columnName], label:solution.label.value, group:solution.group?.value, itemLabel:solution.itemLabel?.value}; + } else { + throw Error("Could not determine which column to read from the result set") + } } } } @@ -145,15 +149,14 @@ export class SparqlListDataProvider implements ListDataProviderIfc { getRdfTermColumn(aBindingSet: any): string | undefined { let foundKey: string | undefined = undefined; for (const key of Object.keys(aBindingSet)) { - if (key != "label") { - if (!foundKey) { - foundKey = key; - } else { - // it means there are more than one column, don't know which one to take, break - return undefined; + if (key != "label") { + if (!foundKey) { + foundKey = key; + } else { + // it means there are more than one column, don't know which one to take, break + return undefined; + } } - } - // console.log(`${key}: ${(aBindingSet as {[key: string]: string})[key]}`); } return foundKey; } @@ -623,3 +626,54 @@ export class SortTreeDataProvider implements TreeDataProviderIfc { } } + +/** + * @param items Merges the datasource items based on their equality, in the case that multiple groups + * (= multiple datasets) return the same RDF term (= the same URI or literal value). In that case a single result is kept, + * with a group that is the concatenation of the groups of the merged items. + * @returns a new list of datasource items in which the items have been merge based on their rdfTerm equality. + */ +export function mergeDatasourceResults(items:RdfTermDatasourceItem[]):RdfTermDatasourceItem[] { + let result:RdfTermDatasourceItem[] = new Array(); + + // iterate on each item + items.forEach(item => { + // if it wasn't already added... + if(!result.some(itemInResult => sameTerm(itemInResult.term, item.term))) { + // find all items with the same URI + let sameTerms = items.filter(i => sameTerm(item.term, i.term)); + // add the first identical item of this in our result table, with a merged group + let newTerm:RdfTermDatasourceItem = { + term: sameTerms[0].term, + label : sameTerms[0].label, + itemLabel: sameTerms[0].itemLabel, + group: sameTerms.map(i => i.group).join(" + ") + } + result.push(newTerm); + } + + }); + + return result; +} + +/** + * @param t1 + * @param t2 + * @returns true if both RDF term are equal (same type, same value, same datatype, same language) + */ +export function sameTerm(t1:RDFTerm, t2:RDFTerm):boolean { + return( + t1 != null + && + t2 != null + && + t1.type == t2.type + && + t1.value == t2.value + && + t1.datatype == t2.datatype + && + t1["xml:lang"] == t2["xml:lang"] + ); +} \ No newline at end of file diff --git a/src/sparnatural/generators/sparql/fromjson/BranchTranslator.ts b/src/sparnatural/generators/sparql/fromjson/BranchTranslator.ts index 3c2c3969..80000a5b 100644 --- a/src/sparnatural/generators/sparql/fromjson/BranchTranslator.ts +++ b/src/sparnatural/generators/sparql/fromjson/BranchTranslator.ts @@ -136,46 +136,43 @@ export default class BranchTranslator { * Generates the triple of the type of subject ("s" and "sType" in the branch JSON structure) */ #buildSubjectClassPtrn() { - if (!this.#valueBuilder?.isBlockingStart()) { - let typeTranslator: TypedVariableTranslator = new TypedVariableTranslator( - this.#branch.line.s, - this.#branch.line.sType, - // Note : on subject position, the only variable that can be selected is the very first one - // Otherwise it can be selected in the object position, but not inside a WHERE clause - // Anyway if it not the very first, all the startClassPtrn is ignored when building the final query - BranchTranslator.isVarSelected(this.#fullQuery, this.#branch.line.s) && - this.#isVeryFirst, - this.#specProvider, - this.settings + let typeTranslator: TypedVariableTranslator = new TypedVariableTranslator( + this.#branch.line.s, + this.#branch.line.sType, + // Note : on subject position, the only variable that can be selected is the very first one + // Otherwise it can be selected in the object position, but not inside a WHERE clause + // Anyway if it not the very first, all the startClassPtrn is ignored when building the final query + BranchTranslator.isVarSelected(this.#fullQuery, this.#branch.line.s) && this.#isVeryFirst, + this.#valueBuilder?.isBlockingStart(), + this.#specProvider, + this.settings + ); + typeTranslator.build(); + this.#startClassPtrn = typeTranslator.resultPtrns; + // if there was any default label patterns generated, gather the variable names of the default label + if (typeTranslator.defaultLblPatterns.length > 0) { + this.#defaultVars.push( + factory.variable(typeTranslator.defaultLabelVarName) ); - typeTranslator.build(); - this.#startClassPtrn = typeTranslator.resultPtrns; - // if there was any default label patterns generated, gather the variable names of the default label - if (typeTranslator.defaultLblPatterns.length > 0) { - this.#defaultVars.push( - factory.variable(typeTranslator.defaultLabelVarName) - ); - } } } #buildObjectClassPtrn() { - if (!this.#valueBuilder?.isBlockingEnd()) { - let typeTranslator: TypedVariableTranslator = new TypedVariableTranslator( - this.#branch.line.o, - this.#branch.line.oType, - BranchTranslator.isVarSelected(this.#fullQuery, this.#branch.line.o), - this.#specProvider, - this.settings - ); - typeTranslator.build(); + let typeTranslator: TypedVariableTranslator = new TypedVariableTranslator( + this.#branch.line.o, + this.#branch.line.oType, + BranchTranslator.isVarSelected(this.#fullQuery, this.#branch.line.o), + this.#valueBuilder?.isBlockingEnd(), + this.#specProvider, + this.settings + ); + typeTranslator.build(); - this.#endClassPtrn = typeTranslator.resultPtrns; - if (typeTranslator.defaultLblPatterns.length > 0) { - this.#defaultVars.push( - factory.variable(typeTranslator.defaultLabelVarName) - ); - } + this.#endClassPtrn = typeTranslator.resultPtrns; + if (typeTranslator.defaultLblPatterns.length > 0) { + this.#defaultVars.push( + factory.variable(typeTranslator.defaultLabelVarName) + ); } } #buildIntersectionPtrn() { diff --git a/src/sparnatural/generators/sparql/fromjson/TypedVariableTranslator.ts b/src/sparnatural/generators/sparql/fromjson/TypedVariableTranslator.ts index 779b6856..873e976b 100644 --- a/src/sparnatural/generators/sparql/fromjson/TypedVariableTranslator.ts +++ b/src/sparnatural/generators/sparql/fromjson/TypedVariableTranslator.ts @@ -33,6 +33,8 @@ export default class TypedVariableTranslator { public defaultLblPatterns: Pattern[] = []; // the rdf:type triple #typeTriple: Triple; + // whether the property is blocking the generation of the type triple (but not the default label triples) + #propertyIsBlocking: boolean; public executedAfterPtrns: Pattern[] = []; @@ -40,11 +42,13 @@ export default class TypedVariableTranslator { variableName: string, variableType: string, variableIsSelected: boolean, + propertyIsBlocking: boolean, specProvider: ISparnaturalSpecification, settings: any ) { this.#variableName = variableName; this.#variableType = variableType; + this.#propertyIsBlocking = propertyIsBlocking; this.#specProvider = specProvider; this.#variableIsSelected = variableIsSelected; @@ -68,28 +72,6 @@ export default class TypedVariableTranslator { /** * Generates the triple of the type */ - #buildTypeTriple1() { - // don't build the class triple if the entity does not have a type criteria - if (this.#specProvider.getEntity(this.#variableType).hasTypeCriteria()) { - var typePredicate; - if (this.settings.typePredicate) { - typePredicate = SparqlFactory.parsePropertyPath( - this.settings.typePredicate - ); - } else { - typePredicate = factory.namedNode( - "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" - ); - } - - this.#typeTriple = SparqlFactory.buildTypeTriple( - factory.variable(this.#variableName), - typePredicate, - factory.namedNode(this.#variableType) - ); - } - } - //------------------------------------------------------------- /** * Test diff --git a/src/sparnatural/spec-providers/shacl/SHACLSpecificationProvider.ts b/src/sparnatural/spec-providers/shacl/SHACLSpecificationProvider.ts index c94c2e22..8ddbd64d 100644 --- a/src/sparnatural/spec-providers/shacl/SHACLSpecificationProvider.ts +++ b/src/sparnatural/spec-providers/shacl/SHACLSpecificationProvider.ts @@ -318,8 +318,9 @@ export class SHACLSpecificationProvider extends BaseRDFReader implements ISparna // replace the $this with the name of the original variable in the query // \S matches any non-whitespace charracter - // note how we match optionnally the final dot of the triple - var re = new RegExp("(\\S*) (rdf:type|a|) <" + nodeShapeUri + ">( \\.)?", "g"); + // note how we match optionnally the final dot of the triple, and an optional space after the type (not always here) + // flag "g" is for global search + var re = new RegExp("(\\S*) (rdf:type|a|) <" + nodeShapeUri + ">( ?\\.)?", "g"); let replacer = function( match:string, @@ -336,6 +337,11 @@ export class SHACLSpecificationProvider extends BaseRDFReader implements ISparna // replacing "$this" with the original variable name var reThis = new RegExp("\\$this", "g"); let whereClauseReplacedThis = whereClauseReplacedVariables.replace(reThis, p1); + + // then we make sure the where clause properly ends with a dot + if(!whereClauseReplacedThis.trim().endsWith(".")) { + whereClauseReplacedThis += "."; + } return whereClauseReplacedThis; } @@ -344,7 +350,7 @@ export class SHACLSpecificationProvider extends BaseRDFReader implements ISparna }); // reparse the query, apply prefixes, and reserialize the query - console.log(sparql) + // console.log(sparql) var query = this.#parser.parse(sparql); for (var key in prefixes) { query.prefixes[key] = prefixes[key];