diff --git a/build.sbt b/build.sbt index 62b431dea7..66c6303d18 100644 --- a/build.sbt +++ b/build.sbt @@ -107,7 +107,7 @@ lazy val core = (project in file("silk-core")) .settings(commonSettings: _*) .settings( name := "Silk Core", - libraryDependencies += "com.typesafe" % "config" % "1.4.2", // Should always use the same version as the Play Framework dependency + libraryDependencies += "com.typesafe" % "config" % "1.4.3", // Should always use the same version as the Play Framework dependency // Additional scala standard libraries libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.2.0", libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value, @@ -207,7 +207,7 @@ lazy val serializationJson = (project in file("silk-plugins/silk-serialization-j .settings( name := "Silk Serialization JSON", libraryDependencies += "com.typesafe.play" %% "play-json" % "2.10.6", - libraryDependencies += "io.swagger.core.v3" % "swagger-annotations" % "2.2.23" + libraryDependencies += "io.swagger.core.v3" % "swagger-annotations" % "2.2.27" ) lazy val persistentCaching = (project in file("silk-plugins/silk-persistent-caching")) @@ -396,9 +396,9 @@ lazy val workbenchOpenApi = (project in file("silk-workbench/silk-workbench-open .settings( name := "Silk Workbench OpenAPI", libraryDependencies += "io.kinoplan" %% "swagger-play" % "0.0.5" exclude("org.scala-lang.modules", "scala-java8-compat_2.13") , - libraryDependencies += "io.swagger.parser.v3" % "swagger-parser-v3" % "2.1.22", - libraryDependencies += "com.networknt" % "json-schema-validator" % "1.0.78", - libraryDependencies += "org.webjars" % "swagger-ui" % "5.17.14" + libraryDependencies += "io.swagger.parser.v3" % "swagger-parser-v3" % "2.1.24", + libraryDependencies += "com.networknt" % "json-schema-validator" % "1.5.4", + libraryDependencies += "org.webjars" % "swagger-ui" % "5.18.2" ) lazy val workbench = (project in file("silk-workbench")) @@ -427,7 +427,7 @@ lazy val singlemachine = (project in file("silk-tools/silk-singlemachine")) .settings(commonSettings: _*) .settings( name := "Silk SingleMachine", - libraryDependencies += "org.slf4j" % "slf4j-jdk14" % "2.0.5" + libraryDependencies += "org.slf4j" % "slf4j-jdk14" % "2.0.16" ) //lazy val mapreduce = (project in file("silk-tools/silk-mapreduce")) diff --git a/libs/gui-elements b/libs/gui-elements index 225b64fb6c..ac704c537e 160000 --- a/libs/gui-elements +++ b/libs/gui-elements @@ -1 +1 @@ -Subproject commit 225b64fb6c326f97f80e8dcf4a6e357e1b02dc14 +Subproject commit ac704c537e449a5a5c70b2e205534141e2896d61 diff --git a/project/build.properties b/project/build.properties index c7450fc2a9..0a832a2dec 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.5 \ No newline at end of file +sbt.version=1.10.7 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index e64528d91c..e7d0320ad2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.5") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.6") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5") diff --git a/silk-core/src/main/scala/org/silkframework/config/ConfigValue.scala b/silk-core/src/main/scala/org/silkframework/config/ConfigValue.scala index baff6d44bf..a3d229cf37 100644 --- a/silk-core/src/main/scala/org/silkframework/config/ConfigValue.scala +++ b/silk-core/src/main/scala/org/silkframework/config/ConfigValue.scala @@ -48,13 +48,13 @@ abstract class ConfigValue[T]() { * @tparam CLASS The class that holds the configuration. * @tparam T The configuration value type. */ -abstract class ClassConfigValue[CLASS: ClassTag, T]() extends ConfigValue[T] { +abstract class ClassConfigValue[T: ClassTag]() extends ConfigValue[T] { /** * Retrieves the configuration for the given class. */ protected override def config: TypesafeConfig = { - DefaultConfig.instance.forClass(implicitly[ClassTag[CLASS]].runtimeClass) + DefaultConfig.instance.forClass(implicitly[ClassTag[T]].runtimeClass) } } diff --git a/silk-core/src/main/scala/org/silkframework/entity/paths/Path.scala b/silk-core/src/main/scala/org/silkframework/entity/paths/Path.scala index 6d678a329e..495f99751c 100644 --- a/silk-core/src/main/scala/org/silkframework/entity/paths/Path.scala +++ b/silk-core/src/main/scala/org/silkframework/entity/paths/Path.scala @@ -13,6 +13,10 @@ trait Path extends Serializable { */ def operators: List[PathOperator] + /** + * Returns a copy with updated operators. + */ + def withOperators(newOperators: List[PathOperator]): Path /** * The normalized serialization using the Silk RDF path language. diff --git a/silk-core/src/main/scala/org/silkframework/entity/paths/PathWithMetadata.scala b/silk-core/src/main/scala/org/silkframework/entity/paths/PathWithMetadata.scala index fbba31189e..e6b57c479a 100644 --- a/silk-core/src/main/scala/org/silkframework/entity/paths/PathWithMetadata.scala +++ b/silk-core/src/main/scala/org/silkframework/entity/paths/PathWithMetadata.scala @@ -33,6 +33,10 @@ class PathWithMetadata ( */ def getOriginalName: String = metadata(PathWithMetadata.META_FIELD_ORIGIN_NAME).toString + override def withOperators(newOperators: List[PathOperator]): PathWithMetadata = { + new PathWithMetadata(newOperators, valueType, metadata) + } + } object PathWithMetadata{ diff --git a/silk-core/src/main/scala/org/silkframework/entity/paths/TypedPath.scala b/silk-core/src/main/scala/org/silkframework/entity/paths/TypedPath.scala index a0d46969d6..15fc75b758 100644 --- a/silk-core/src/main/scala/org/silkframework/entity/paths/TypedPath.scala +++ b/silk-core/src/main/scala/org/silkframework/entity/paths/TypedPath.scala @@ -69,6 +69,10 @@ case class TypedPath( } override def toString: String = s"$normalizedSerialization (${valueType.label})" + + override def withOperators(newOperators: List[PathOperator]): TypedPath = { + copy(operators = newOperators) + } } object TypedPath { diff --git a/silk-core/src/main/scala/org/silkframework/entity/paths/UntypedPath.scala b/silk-core/src/main/scala/org/silkframework/entity/paths/UntypedPath.scala index 83c418256c..465097c339 100644 --- a/silk-core/src/main/scala/org/silkframework/entity/paths/UntypedPath.scala +++ b/silk-core/src/main/scala/org/silkframework/entity/paths/UntypedPath.scala @@ -52,6 +52,10 @@ class UntypedPath private[entity](val operators: List[PathOperator]) extends Pat /** Returns an untyped ([[org.silkframework.entity.UntypedValueType]]) [[TypedPath]]. */ def asUntypedValueType: TypedPath = TypedPath(this.operators, ValueType.UNTYPED, isAttribute = false) + + override def withOperators(newOperators: List[PathOperator]): UntypedPath = { + new UntypedPath(operators = newOperators) + } } object UntypedPath { diff --git a/silk-core/src/main/scala/org/silkframework/util/Identifier.scala b/silk-core/src/main/scala/org/silkframework/util/Identifier.scala index 3b90bb0f36..642748a846 100644 --- a/silk-core/src/main/scala/org/silkframework/util/Identifier.scala +++ b/silk-core/src/main/scala/org/silkframework/util/Identifier.scala @@ -63,6 +63,8 @@ object Identifier { (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_') || (c == '-') } + def filterAllowedChars(str: String): String = str.filter(Identifier.isAllowed) + /** * Creates a new Identifier only from the allowed characters in a given string. * @@ -71,7 +73,7 @@ object Identifier { */ def fromAllowed(str: String, alternative: Option[String] = None): Identifier = { try { - new Identifier(str.filter(Identifier.isAllowed)) + new Identifier(filterAllowedChars(str)) } catch { case _: IllegalArgumentException if alternative.isDefined => Identifier(alternative.get) diff --git a/silk-core/src/main/scala/org/silkframework/util/IdentifierGenerator.scala b/silk-core/src/main/scala/org/silkframework/util/IdentifierGenerator.scala index 589d012d94..4045f3c454 100644 --- a/silk-core/src/main/scala/org/silkframework/util/IdentifierGenerator.scala +++ b/silk-core/src/main/scala/org/silkframework/util/IdentifierGenerator.scala @@ -2,26 +2,41 @@ package org.silkframework.util /** * Generates identifiers that are unique in its scope. + * Thread-safe. */ -class IdentifierGenerator(prefix: String = "") { +class IdentifierGenerator { /** - * Remembers all existing identifiers and their counts. - */ - private var identifiers = Map[String, Int]() + * Remembers all existing identifiers. + */ + private var identifiers = Set[Identifier]() + // Default identifier if the desired ID contains no allowed characters. + final val UNNAMED_ID = "unnamed_id" /** * Generates a new unique identifier. + * + * @param desiredId The desired identifier. Invalid characters will be removed, so it's not required that this is a valid identifier. + * @return If the provided identifier is already unique, it will be returned unchanged. Otherwise, a unique identifier is generated by appending a number. */ - def generate(identifier: String): Identifier = synchronized { - val (name, num) = split(Identifier.fromAllowed(prefix + identifier)) - identifiers.get(name) match { - case Some(count) => - identifiers += ((name, count + 1)) - name + count.toString - case None => - identifiers += ((name, 1)) - name + def generate(desiredId: String): Identifier = synchronized { + val desiredIdOnlyAllowedChars = Identifier.filterAllowedChars(desiredId) + val identifier = Identifier(if(desiredIdOnlyAllowedChars.isEmpty) UNNAMED_ID else desiredIdOnlyAllowedChars) + if(!identifiers.contains(identifier)) { + // Identifier is already unique + identifiers += identifier + identifier + } else { + // Generate a new identifier by adding a unique number + val (name, num) = split(identifier) + for(i <- Iterator.from(num + 1)) { + val newIdentifier = name + i + if(!identifiers.contains(newIdentifier)) { + identifiers += newIdentifier + return newIdentifier + } + } + throw new IllegalStateException() } } @@ -29,13 +44,7 @@ class IdentifierGenerator(prefix: String = "") { * Adds an existing identifier without changing it. */ def add(identifier: Identifier): Unit = synchronized { - val (name, num) = split(identifier) - identifiers.get(name.toString) match { - case Some(count) => - identifiers += ((name, math.max(count, num + 1))) - case None => - identifiers += ((name, num + 1)) - } + identifiers += identifier } /** @@ -51,3 +60,14 @@ class IdentifierGenerator(prefix: String = "") { } } + +/** + * Adds a prefix to all generated identifiers. + */ +class PrefixedIdentifierGenerator(prefix: String) extends IdentifierGenerator { + + override def generate(desiredId: String): Identifier = { + super.generate(prefix + desiredId) + } + +} \ No newline at end of file diff --git a/silk-core/src/main/scala/org/silkframework/workspace/WorkspaceProvider.scala b/silk-core/src/main/scala/org/silkframework/workspace/WorkspaceProvider.scala index f256cd1838..aef066ca27 100644 --- a/silk-core/src/main/scala/org/silkframework/workspace/WorkspaceProvider.scala +++ b/silk-core/src/main/scala/org/silkframework/workspace/WorkspaceProvider.scala @@ -1,7 +1,7 @@ package org.silkframework.workspace import org.silkframework.config._ -import org.silkframework.dataset.rdf.SparqlEndpoint +import org.silkframework.dataset.rdf.{GraphStoreTrait, SparqlEndpoint} import org.silkframework.runtime.activity.UserContext import org.silkframework.runtime.plugin.annotations.PluginType import org.silkframework.runtime.plugin.{AnyPlugin, ParameterValues, PluginContext} @@ -16,7 +16,7 @@ import scala.collection.mutable import scala.language.implicitConversions import scala.reflect.ClassTag import scala.util.control.NonFatal -import scala.xml.{Attribute, Elem, Node, Text, Null} +import scala.xml.{Attribute, Elem, Node, Null, Text} @PluginType() trait WorkspaceProvider extends AnyPlugin { @@ -146,7 +146,7 @@ trait WorkspaceProvider extends AnyPlugin { * Returns an SPARQL endpoint that allows query access to the projects. * May return None if the projects are not held as RDF. */ - def sparqlEndpoint: Option[SparqlEndpoint] + def sparqlEndpoint: Option[SparqlEndpoint with GraphStoreTrait] private val externalLoadingErrors: mutable.HashMap[String, Vector[TaskLoadingError]] = new mutable.HashMap[String, Vector[TaskLoadingError]]() diff --git a/silk-core/src/test/scala/org/silkframework/util/IdentifierGeneratorTest.scala b/silk-core/src/test/scala/org/silkframework/util/IdentifierGeneratorTest.scala index 6061be5d8e..c7a19f6e5e 100644 --- a/silk-core/src/test/scala/org/silkframework/util/IdentifierGeneratorTest.scala +++ b/silk-core/src/test/scala/org/silkframework/util/IdentifierGeneratorTest.scala @@ -34,4 +34,9 @@ class IdentifierGeneratorTest extends AnyFlatSpec with Matchers { generator.generate("person") mustBe Identifier("person2") generator.generate("person1") mustBe Identifier("person3") } + + it should "not remove trailing numbers if not needed for generating a unique id" in { + val generator = new IdentifierGenerator() + generator.generate("name1") mustBe Identifier("name1") + } } diff --git a/silk-legacy-ui/package.json b/silk-legacy-ui/package.json index 790419cbe7..ca9d7246cf 100644 --- a/silk-legacy-ui/package.json +++ b/silk-legacy-ui/package.json @@ -59,7 +59,7 @@ "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.1.2", "@testing-library/user-event": "^12.2.2", - "@types/carbon-components-react": "7.44.1", + "@types/carbon-components-react": "7.55.2", "@types/codemirror": "0.0.109", "@types/enzyme": "^3.10.8", "@types/enzyme-adapter-react-16": "^1.0.6", diff --git a/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfFilesVocabularyManager.scala b/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfFilesVocabularyManager.scala index 5b20e76473..ba19b06608 100644 --- a/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfFilesVocabularyManager.scala +++ b/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfFilesVocabularyManager.scala @@ -1,7 +1,5 @@ package org.silkframework.plugins.dataset.rdf.vocab -import java.util.logging.Logger - import org.apache.jena.query.DatasetFactory import org.apache.jena.rdf.model.ModelFactory import org.apache.jena.riot.{RDFDataMgr, RDFLanguages} @@ -9,10 +7,12 @@ import org.silkframework.plugins.dataset.rdf.endpoint.JenaDatasetEndpoint import org.silkframework.rule.vocab.{Vocabulary, VocabularyManager} import org.silkframework.runtime.activity.UserContext import org.silkframework.runtime.plugin.annotations.Plugin -import org.silkframework.runtime.resource.{FileResource, ResourceNotFoundException} +import org.silkframework.runtime.resource.{Resource, ResourceNotFoundException} import org.silkframework.util.Identifier import org.silkframework.workspace.WorkspaceFactory +import java.util.logging.Logger + @Plugin( id = "rdfFiles", label = "RDF Files", @@ -31,20 +31,7 @@ case class RdfFilesVocabularyManager() extends VocabularyManager { if(vocabularyResource.nonEmpty && vocabularyResource.get.size.nonEmpty && !vocabularyResource.get.size.contains(0L)) { // only consider files val resource = vocabularyResource.get - // Load into Jena model - val model = ModelFactory.createDefaultModel() - val inputStream = resource.inputStream - RDFDataMgr.read(model, inputStream, RDFLanguages.filenameToLang(resource.name)) - inputStream.close() - - // Create vocabulary loader - val dataset = DatasetFactory.createTxnMem() - dataset.addNamedModel(prefix + uri, model) - val endpoint = new JenaDatasetEndpoint(dataset) - val loader = new VocabularyLoader(endpoint) - - // Load vocabulary - loader.retrieveVocabulary(prefix + uri) + loadVocabulary(resource, uri) } else { None } @@ -55,6 +42,27 @@ case class RdfFilesVocabularyManager() extends VocabularyManager { } } + /** + * Loads a vocabulary from a file. + */ + def loadVocabulary(resource: Resource, uri: String) + (implicit userContext: UserContext): Option[Vocabulary] = { + // Load into Jena model + val model = ModelFactory.createDefaultModel() + val inputStream = resource.inputStream + RDFDataMgr.read(model, inputStream, RDFLanguages.filenameToLang(resource.name)) + inputStream.close() + + // Create vocabulary loader + val dataset = DatasetFactory.createTxnMem() + dataset.addNamedModel(prefix + uri, model) + val endpoint = new JenaDatasetEndpoint(dataset) + val loader = new VocabularyLoader(endpoint) + + // Load vocabulary + loader.retrieveVocabulary(prefix + uri) + } + override def retrieveGlobalVocabularies()(implicit userContext: UserContext): Option[Iterable[String]] = { // FIXME: Not clear how to automatically decide which RDF files are global vocabularies without registering them. None diff --git a/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfVocabularyManager.scala b/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfVocabularyManager.scala index 431757d6c5..3e0fca588b 100644 --- a/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfVocabularyManager.scala +++ b/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/RdfVocabularyManager.scala @@ -1,6 +1,6 @@ package org.silkframework.plugins.dataset.rdf.vocab -import org.silkframework.dataset.rdf.SparqlEndpoint +import org.silkframework.dataset.rdf.{GraphStoreTrait, SparqlEndpoint} import org.silkframework.rule.vocab.{Vocabulary, VocabularyManager} import org.silkframework.runtime.activity.UserContext import org.silkframework.runtime.plugin.annotations.Plugin @@ -20,7 +20,7 @@ case class RdfVocabularyManager() extends VocabularyManager { loader.retrieveVocabulary(uri) } - private def workspaceSparqlEndpoint(implicit userContext: UserContext): SparqlEndpoint = { + private def workspaceSparqlEndpoint(implicit userContext: UserContext): SparqlEndpoint with GraphStoreTrait = { WorkspaceFactory().workspace.provider.sparqlEndpoint match { case Some(endpoint) => endpoint diff --git a/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/VocabularyLoader.scala b/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/VocabularyLoader.scala index 488cfd3a0b..e6986e76bb 100644 --- a/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/VocabularyLoader.scala +++ b/silk-plugins/silk-plugins-rdf/src/main/scala/org/silkframework/plugins/dataset/rdf/vocab/VocabularyLoader.scala @@ -8,7 +8,7 @@ import org.silkframework.runtime.activity.UserContext import scala.collection.immutable.SortedMap import scala.collection.mutable -class VocabularyLoader(endpoint: SparqlEndpoint) { +class VocabularyLoader(endpoint: SparqlEndpoint with GraphStoreTrait) { final val languageRanking: IndexedSeq[String] = IndexedSeq("en", "de", "es", "fr", "it", "pt") def retrieveVocabulary(uri: String)(implicit userContext: UserContext): Option[Vocabulary] = { @@ -17,7 +17,8 @@ class VocabularyLoader(endpoint: SparqlEndpoint) { Some(Vocabulary( info = vocabGenericInfo, classes = classes.toSeq, - properties = retrieveProperties(uri, classes.toSeq) + properties = retrieveProperties(uri, classes.toSeq), + endpoint = Some(endpoint) )) } diff --git a/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/GenericInfoJson.scala b/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/GenericInfoJson.scala new file mode 100644 index 0000000000..05175d44cf --- /dev/null +++ b/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/GenericInfoJson.scala @@ -0,0 +1,37 @@ +package org.silkframework.serialization.json + +import io.swagger.v3.oas.annotations.media.Schema +import org.silkframework.rule.vocab.GenericInfo +import org.silkframework.runtime.serialization.{ReadContext, WriteContext} +import org.silkframework.util.Uri +import play.api.libs.json.{Format, JsValue, Json} + +case class GenericInfoJson( + @Schema(description = "The URI or prefixed name of the class") + uri: String, + label: Option[String] = None, + description: Option[String] = None, + altLabels: Seq[String] = Seq.empty +) + +object GenericInfoJson extends JsonCompanion[GenericInfo, GenericInfoJson] { + override implicit lazy val jsonFormat: Format[GenericInfoJson] = Json.format[GenericInfoJson] + + override def read(json: GenericInfoJson)(implicit readContext: ReadContext): GenericInfo = { + GenericInfo( + uri = Uri.parse(json.uri, readContext.prefixes), + label = json.label, + description = json.description, + altLabels = json.altLabels + ) + } + + override def write(data: GenericInfo)(implicit writeContext: WriteContext[JsValue]): GenericInfoJson = { + GenericInfoJson( + uri = writeContext.prefixes.shorten(data.uri), + label = data.label, + description = data.description, + altLabels = data.altLabels + ) + } +} \ No newline at end of file diff --git a/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/JsonCompanion.scala b/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/JsonCompanion.scala new file mode 100644 index 0000000000..8f7dcc7679 --- /dev/null +++ b/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/JsonCompanion.scala @@ -0,0 +1,28 @@ +package org.silkframework.serialization.json + +import org.silkframework.runtime.serialization.{ReadContext, WriteContext} +import play.api.libs.json.{Format, JsValue, Json} + +/** + * A companion object for a data class that can be serialized to and from JSON. + * This is preferred to implementing a JsonFormat directly, since it also allows to annotate the JSON class with OpenAPI annotations and generate the JSON schema. + * + * @tparam DataClass The data class that is serialized to and from JSON. + * @tparam JsonClass The JSON class that is used for serialization. + */ +trait JsonCompanion[DataClass, JsonClass] { + + implicit def jsonFormat: Format[JsonClass] + + def read(json: JsonClass)(implicit readContext: ReadContext): DataClass + + def write(data: DataClass)(implicit writeContext: WriteContext[JsValue]): JsonClass + + def readJson(json: JsValue)(implicit readContext: ReadContext): DataClass = { + read(Json.fromJson[JsonClass](json).get) + } + + def writeJson(data: DataClass)(implicit writeContext: WriteContext[JsValue]): JsValue = { + Json.toJson(write(data)) + } +} \ No newline at end of file diff --git a/silk-rules/src/main/scala/org/silkframework/rule/TransformRule.scala b/silk-rules/src/main/scala/org/silkframework/rule/TransformRule.scala index 2c121903c7..da2e84b67d 100644 --- a/silk-rules/src/main/scala/org/silkframework/rule/TransformRule.scala +++ b/silk-rules/src/main/scala/org/silkframework/rule/TransformRule.scala @@ -278,13 +278,16 @@ object RootMappingRule { * @param id The name of this mapping. For direct mappings usually just the property that is mapped. * @param sourcePath The source path * @param mappingTarget The target property + * @param metaData Meta data + * @param inputId Identifier of the generated input path operator. If not defined, the mapping `id` will be used. */ case class DirectMapping(id: Identifier = "sourcePath", sourcePath: UntypedPath = UntypedPath(Nil), mappingTarget: MappingTarget = MappingTarget("http://www.w3.org/2000/01/rdf-schema#label"), - metaData: MetaData = MetaData.empty) extends ValueTransformRule { + metaData: MetaData = MetaData.empty, + inputId: Option[Identifier] = None) extends ValueTransformRule { - override val operator = PathInput(id, sourcePath.asUntypedPath) + override val operator = PathInput(inputId.getOrElse(id), sourcePath.asUntypedPath) override val target = Some(mappingTarget) @@ -555,8 +558,8 @@ object TransformRule { */ def simplify(complexMapping: ComplexMapping)(implicit prefixes: Prefixes): TransformRule = complexMapping match { // Direct Mapping - case ComplexMapping(id, PathInput(_, path), Some(target), metaData, _, uiAnnotations) if uiAnnotations.stickyNotes.isEmpty => - DirectMapping(id, path.asUntypedPath, target, metaData) + case ComplexMapping(id, PathInput(pathId, path), Some(target), metaData, _, uiAnnotations) if uiAnnotations.stickyNotes.isEmpty => + DirectMapping(id, path.asUntypedPath, target, metaData, Some(pathId)) // Rule with annotations or layout info is always treated as complex (URI) mapping rule case ComplexMapping(id, operator, targetOpt, metaData, layout, uiAnnotations) if layout.nodePositions.nonEmpty || uiAnnotations.stickyNotes.nonEmpty => if(targetOpt.isEmpty) { diff --git a/silk-rules/src/main/scala/org/silkframework/rule/vocab/Vocabulary.scala b/silk-rules/src/main/scala/org/silkframework/rule/vocab/Vocabulary.scala index aa431761d5..72931c8964 100644 --- a/silk-rules/src/main/scala/org/silkframework/rule/vocab/Vocabulary.scala +++ b/silk-rules/src/main/scala/org/silkframework/rule/vocab/Vocabulary.scala @@ -1,13 +1,17 @@ package org.silkframework.rule.vocab -import java.util.logging.Logger +import org.silkframework.dataset.rdf.{GraphStoreTrait, SparqlEndpoint} +import java.util.logging.Logger import org.silkframework.rule.vocab.GenericInfo.GenericInfoFormat import org.silkframework.runtime.serialization.{ReadContext, WriteContext, XmlFormat} import scala.xml.{Node, Null} -case class Vocabulary(info: GenericInfo, classes: Iterable[VocabularyClass], properties: Iterable[VocabularyProperty]) { +case class Vocabulary(info: GenericInfo, + classes: Iterable[VocabularyClass], + properties: Iterable[VocabularyProperty], + endpoint: Option[SparqlEndpoint with GraphStoreTrait] = None) { def getClass(uri: String): Option[VocabularyClass] = { classes.find(_.info.uri == uri) @@ -32,7 +36,8 @@ object Vocabulary { Vocabulary( info = GenericInfoFormat.read((node \ INFO).head), classes = classes, - properties = properties + properties = properties, + endpoint = None ) } diff --git a/silk-rules/src/main/scala/org/silkframework/rule/vocab/VocabularyManager.scala b/silk-rules/src/main/scala/org/silkframework/rule/vocab/VocabularyManager.scala index 612b8dea3f..7fcfee7400 100644 --- a/silk-rules/src/main/scala/org/silkframework/rule/vocab/VocabularyManager.scala +++ b/silk-rules/src/main/scala/org/silkframework/rule/vocab/VocabularyManager.scala @@ -6,6 +6,8 @@ import org.silkframework.runtime.plugin.annotations.PluginType import org.silkframework.runtime.plugin.{AnyPlugin, PluginContext, PluginRegistry} import org.silkframework.util.Identifier +import java.util.logging.Logger + @PluginType() trait VocabularyManager extends AnyPlugin { @@ -21,11 +23,13 @@ trait VocabularyManager extends AnyPlugin { object VocabularyManager { private var lastPlugin: String = "" private var vocabularyManager: Option[VocabularyManager] = None + private val log: Logger = Logger.getLogger(getClass.getName) private def instance: VocabularyManager = this.synchronized { implicit val context: PluginContext = PluginContext.empty val plugin = DefaultConfig.instance().getString("vocabulary.manager.plugin") if(plugin != lastPlugin || vocabularyManager.isEmpty) { + log.info("Using configured VocabularyManager plugin: " + plugin) vocabularyManager = Some(PluginRegistry.createFromConfig[VocabularyManager]("vocabulary.manager")) lastPlugin = plugin } diff --git a/silk-rules/src/test/scala/org/silkframework/rule/TransformRuleXmlSerializationTest.scala b/silk-rules/src/test/scala/org/silkframework/rule/TransformRuleXmlSerializationTest.scala index 19d428d4fb..893e453055 100644 --- a/silk-rules/src/test/scala/org/silkframework/rule/TransformRuleXmlSerializationTest.scala +++ b/silk-rules/src/test/scala/org/silkframework/rule/TransformRuleXmlSerializationTest.scala @@ -14,7 +14,7 @@ class TransformRuleXmlSerializationTest extends AnyFlatSpec with Matchers { behavior of "TransformRule.XmlFormat" it should "serialize direct mappings" in { - testSerialzation(DirectMapping("directMapping", UntypedPath("inputPath"), MappingTarget("outputProperty", ValueType.STRING))) + testSerialzation(DirectMapping("directMapping", UntypedPath("inputPath"), MappingTarget("outputProperty", ValueType.STRING), inputId = Some("inputPath"))) } it should "serialize object mappings" in { @@ -24,7 +24,7 @@ class TransformRuleXmlSerializationTest extends AnyFlatSpec with Matchers { sourcePath = UntypedPath("relativePath"), target = Some(MappingTarget("targetProperty")), rules = MappingRules( - DirectMapping("directMapping", UntypedPath("inputPath"), MappingTarget("outputProperty", ValueType.STRING)) + DirectMapping("directMapping", UntypedPath("inputPath"), MappingTarget("outputProperty", ValueType.STRING), inputId = Some("inputPath")) ) ) ) diff --git a/silk-workbench/silk-workbench-rules/app/controllers/linking/LinkingAutoCompletionApi.scala b/silk-workbench/silk-workbench-rules/app/controllers/linking/LinkingAutoCompletionApi.scala index 1ca18dd2ab..041eefdb6e 100644 --- a/silk-workbench/silk-workbench-rules/app/controllers/linking/LinkingAutoCompletionApi.scala +++ b/silk-workbench/silk-workbench-rules/app/controllers/linking/LinkingAutoCompletionApi.scala @@ -197,7 +197,7 @@ class LinkingAutoCompletionApi @Inject() () extends InjectedController with User (implicit userContext: UserContext): AutoSuggestAutoCompletionResponse = { implicit val project: Project = linkingTask.project implicit val prefixes: Prefixes = project.config.prefixes - val isRdfInput = DatasetUtils.isRdfInput(project, datasetSelection) + val isRdfInput = TransformUtils.isRdfDataset(project, datasetSelection) val pathToReplace = PartialSourcePathAutocompletionHelper.pathToReplace(autoCompletionRequest, isRdfInput) val dataSourceCharacteristicsOpt = DatasetUtils.datasetCharacteristics(project, datasetSelection) val supportsAsteriskOperator = dataSourceCharacteristicsOpt.exists(_.supportsAsteriskPathOperator) diff --git a/silk-workbench/silk-workbench-rules/app/controllers/transform/AutoCompletionApi.scala b/silk-workbench/silk-workbench-rules/app/controllers/transform/AutoCompletionApi.scala index de5b7a3889..461724ec96 100644 --- a/silk-workbench/silk-workbench-rules/app/controllers/transform/AutoCompletionApi.scala +++ b/silk-workbench/silk-workbench-rules/app/controllers/transform/AutoCompletionApi.scala @@ -27,7 +27,7 @@ import org.silkframework.serialization.json.JsonHelpers import org.silkframework.workspace.activity.dataset.DatasetUtils import org.silkframework.workspace.activity.transform.{TransformPathsCache, VocabularyCacheValue} import org.silkframework.workspace.{Project, ProjectTask, WorkspaceFactory} -import play.api.libs.json.{JsValue, Json} +import play.api.libs.json.{JsString, JsValue, Json} import play.api.mvc._ import java.util.logging.Logger @@ -258,11 +258,18 @@ class AutoCompletionApi @Inject() () extends InjectedController with UserContext val inputEqualsConfig = inputAndOutputTasks.inputTasks.size == 1 && inputAndOutputTasks.inputTasks.head.workflowContextTask.id == transformTask.data.selection.inputId.toString val alternativeEntitySchema = if(inputEqualsConfig) None else inputAndOutputTasks.inputEntitySchema().filter(_.typedPaths.nonEmpty) - withRule(transformTask, ruleId) { case (_, sourcePath) => - val autoCompletionResponse = autoCompletePartialSourcePath(transformTask, autoCompletionRequest, sourcePath, - autoCompletionRequest.isObjectPath.getOrElse(false), alternativeEntitySchema) - Ok(Json.toJson(autoCompletionResponse)) + val sourcePath: List[PathOperator] = autoCompletionRequest.baseSourcePath match { + case Some(sourcePathString) => + implicit val prefixes: Prefixes = project.config.prefixes + UntypedPath.parse(sourcePathString).operators + case None => + withRule(transformTask, ruleId) { case (_, sourcePath) => + sourcePath + } } + val autoCompletionResponse = autoCompletePartialSourcePath(transformTask, autoCompletionRequest, sourcePath, + autoCompletionRequest.isObjectPath.getOrElse(false), alternativeEntitySchema) + Ok(Json.toJson(autoCompletionResponse)) } } @@ -296,7 +303,8 @@ class AutoCompletionApi @Inject() () extends InjectedController with UserContext case (false, true) => OpFilter.Forward case _ => OpFilter.None } - val relativePaths = AutoCompletionApiUtils.extractRelativePaths(simpleSubPath, forwardOnlySubPath, allPaths, isRdfInput, oneHopOnly = pathToReplace.insideFilter, + val relativePaths = AutoCompletionApiUtils.extractRelativePaths(simpleSubPath, forwardOnlySubPath, allPaths, isRdfInput, + oneHopOnly = pathToReplace.insideFilter || autoCompletionRequest.oneHopOnly.getOrElse(false), serializeFull = !pathToReplace.insideFilter && pathToReplace.from > 0, pathOpFilter = pathOpFilter, supportsAsteriskOperator = supportsAsteriskOperator ) @@ -369,7 +377,10 @@ class AutoCompletionApi @Inject() () extends InjectedController with UserContext uriPatternAutoCompletionRequest.cursorPosition - pathPart.segmentPosition.originalStartIndex, uriPatternAutoCompletionRequest.maxSuggestions, Some(false), - uriPatternAutoCompletionRequest.workflowTaskContext + uriPatternAutoCompletionRequest.workflowTaskContext, + None, + None, + None ) val inputOutputTasks = ProjectTaskApi.validateTaskContext(project, uriPatternAutoCompletionRequest.workflowTaskContext) val inputEqualsConfig = inputOutputTasks.inputTasks.size == 1 && @@ -652,7 +663,7 @@ class AutoCompletionApi @Inject() () extends InjectedController with UserContext val completions = vocabularyTypeCompletions(task) // Removed as they currently cannot be edited in the UI: completions += prefixCompletions(project.config.prefixes) - Ok(completions.filterAndSort(term, maxResults).toJson) + Ok(completions.filterAndSort(term, maxResults, multiWordFilter = true).toJson) } @Operation( @@ -783,16 +794,28 @@ class AutoCompletionApi @Inject() () extends InjectedController with UserContext val propertyCompletions = for(vocab <- vocabularyCache.vocabularies; prop <- vocab.properties) yield { + val propertyType = if (prop.propertyType == ObjectPropertyType) "object" else "value" + var extra = Json.obj( + "type" -> propertyType, + "graph" -> vocab.info.uri // FIXME: Currently the vocab URI and graph URI are the same. This might change in the future. + ) + if(prop.propertyType == ObjectPropertyType && prop.range.isDefined) { + val rangeInfo = prop.range.get.info + var rangeObj = Json.obj( + "uri" -> rangeInfo.uri + ) + rangeInfo.label.foreach { label => + rangeObj = rangeObj + ("label" -> JsString(label)) + } + extra = extra + ("range" -> rangeObj) + } Completion( value = if(fullUris) prop.info.uri else prefixes.shorten(prop.info.uri), label = prop.info.label, description = prop.info.description, category = Categories.vocabularyProperties, isCompletion = true, - extra = Some(Json.obj( - "type" -> (if (prop.propertyType == ObjectPropertyType) "object" else "value"), - "graph" -> vocab.info.uri // FIXME: Currently the vocab URI and graph URI are the same. This might change in the future. - )) + extra = Some(extra) ) } diff --git a/silk-workbench/silk-workbench-rules/app/controllers/transform/TargetVocabularyApi.scala b/silk-workbench/silk-workbench-rules/app/controllers/transform/TargetVocabularyApi.scala index e3819264a2..e216e03054 100644 --- a/silk-workbench/silk-workbench-rules/app/controllers/transform/TargetVocabularyApi.scala +++ b/silk-workbench/silk-workbench-rules/app/controllers/transform/TargetVocabularyApi.scala @@ -3,6 +3,7 @@ package controllers.transform import controllers.core.UserContextActions import controllers.core.util.ControllerUtilsTrait import controllers.transform.doc.TargetVocabularyApiDoc +import controllers.transform.vocabulary.SourcePropertiesOfClassRequest import controllers.util.SerializationUtils._ import controllers.workspace.workspaceRequests.{VocabularyInfo, VocabularyInfos} import io.swagger.v3.oas.annotations.enums.ParameterIn @@ -10,20 +11,22 @@ import io.swagger.v3.oas.annotations.media.{Content, ExampleObject, Schema} import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.{Operation, Parameter} +import org.silkframework.config.Prefixes import org.silkframework.rule.TransformSpec -import org.silkframework.rule.vocab.{VocabularyClass, VocabularyProperty} +import org.silkframework.rule.vocab.{Vocabularies, VocabularyClass, VocabularyProperty} import org.silkframework.runtime.activity.UserContext import org.silkframework.runtime.serialization.WriteContext import org.silkframework.runtime.validation.NotFoundException import org.silkframework.serialization.json.JsonSerializers import org.silkframework.util.Uri import org.silkframework.workbench.utils.ErrorResult -import org.silkframework.workspace.activity.transform.{VocabularyCache, VocabularyCacheValue} +import org.silkframework.workspace.activity.transform.{TransformPathsCache, VocabularyCache, VocabularyCacheValue} import org.silkframework.workspace.{Project, WorkspaceFactory} import play.api.libs.json.{JsValue, Json, Writes} import play.api.mvc.{Action, AnyContent, InjectedController} import javax.inject.Inject +import scala.util.{Success, Try} @Tag(name = "Transform target vocabulary", description = "Provides access to the target vocabulary.") class TargetVocabularyApi @Inject() () extends InjectedController with UserContextActions with ControllerUtilsTrait { @@ -239,12 +242,86 @@ class TargetVocabularyApi @Inject() () extends InjectedController with UserCont in = ParameterIn.QUERY, schema = new Schema(implementation = classOf[String]) ) - classUri: String): Action[AnyContent] = RequestUserContextAction { implicit request => implicit userContext => + classUri: String, + @Parameter( + name = "includeGeneralProperties", + description = "If true then also properties defined on owl:Thing and properties without any domain statement are returned.", + required = false, + in = ParameterIn.QUERY, + schema = new Schema(implementation = classOf[Boolean]) + ) + includeGeneralProperties: Boolean): Action[AnyContent] = RequestUserContextAction { implicit request => implicit userContext => implicit val project: Project = WorkspaceFactory().workspace.project(projectName) - val (vocabularyProps, _) = vocabularyPropertiesByType(taskName, project, classUri, addBackwardRelations = false) + val (vocabularyProps, _) = vocabularyPropertiesByType(taskName, project, fullClassUri(classUri, project.config.prefixes), + addBackwardRelations = false, includeGeneralProperties = includeGeneralProperties) serializeIterableCompileTime(vocabularyProps, containerName = Some("Properties")) } + @Operation( + summary = "Source vocabulary properties by class", + description = "Get all properties that the given class or any of its parent classes are the domain of in the vocabulary.", + responses = Array( + new ApiResponse( + responseCode = "200", + description = "Success", + content = Array( + new Content( + mediaType = "application/json", + examples = Array(new ExampleObject(TargetVocabularyApiDoc.propertiesByClassExample)) + ) + ) + ), + new ApiResponse( + responseCode = "404", + description = "If the specified project, task or class has not been found." + ) + )) + def sourcePropertiesByType(@Parameter( + name = "project", + description = "The project identifier", + required = true, + in = ParameterIn.PATH, + schema = new Schema(implementation = classOf[String]) + ) + projectName: String, + @Parameter( + name = "task", + description = "The task identifier", + required = true, + in = ParameterIn.PATH, + schema = new Schema(implementation = classOf[String]) + ) + taskName: String): Action[JsValue] = RequestUserContextAction(parse.json) { implicit request => + implicit userContext => + validateJson[SourcePropertiesOfClassRequest] { sourcePropertiesOfClassRequest => + implicit val (project, transformTask) = projectAndTask[TransformSpec](projectName, taskName) + var (vocabularyProps, _) = vocabularyPropertiesByType(taskName, project, fullClassUri(sourcePropertiesOfClassRequest.classUri, project.config.prefixes), + addBackwardRelations = false, fromGlobalVocabularyCache = true, includeGeneralProperties = sourcePropertiesOfClassRequest.includeGeneralProperties) + if(sourcePropertiesOfClassRequest.fromPathCacheOnly) { + val pathsCache = transformTask.activity[TransformPathsCache] + pathsCache.value.get match { + case Some(cacheValue) => + val rootProperties = cacheValue.configuredSchema.typedPaths.flatMap(_.property.map(_.propertyUri)).toSet + val otherProperties = cacheValue.untypedSchema.toSeq.flatMap(_.typedPaths.flatMap(_.property.map(_.propertyUri))).toSet + val allProperties = rootProperties ++ otherProperties + vocabularyProps = vocabularyProps.filter(p => allProperties.contains(p.info.uri)) + case None => + // Do not filter + } + } + serializeIterableCompileTime(vocabularyProps, containerName = Some("Properties")) + } + } + + private def fullClassUri(classUri: String, prefixes: Prefixes): String = { + Try(prefixes.resolve(classUri)) match { + case Success(resolvedUri) => + resolvedUri + case _ => + classUri + } + } + /** * Returns all properties that are directly defined on a class or one of its parent classes. * @param classUri The class we want to have the relations for. @@ -254,10 +331,16 @@ class TargetVocabularyApi @Inject() () extends InjectedController with UserCont private def vocabularyPropertiesByType(taskName: String, project: Project, classUri: String, - addBackwardRelations: Boolean) + addBackwardRelations: Boolean, + fromGlobalVocabularyCache: Boolean = false, + includeGeneralProperties: Boolean = false) (implicit userContext: UserContext): (Seq[VocabularyProperty], Seq[VocabularyProperty]) = { val task = project.task[TransformSpec](taskName) - val vocabularies = VocabularyCacheValue.targetVocabularies(task) + val vocabularies: Vocabularies = if(fromGlobalVocabularyCache) { + Vocabularies(VocabularyCacheValue.globalVocabularies) + } else { + VocabularyCacheValue.targetVocabularies(task) + } val vocabularyClasses = vocabularies.flatMap(v => v.getClass(classUri).map(c => (v, c))) def filterProperties(propFilter: (VocabularyProperty, List[String]) => Boolean): Seq[VocabularyProperty] = { @@ -269,8 +352,14 @@ class TargetVocabularyApi @Inject() () extends InjectedController with UserCont props.flatten } - val forwardProperties = filterProperties((prop, classes) => prop.domain.exists(vc => classes.contains(vc.info.uri))) - val backwardProperties = filterProperties((prop, classes) => addBackwardRelations && prop.range.exists(vc => classes.contains(vc.info.uri))) + val forwardProperties = filterProperties((prop, classes) => { + prop.domain.exists(vc => { + classes.contains(vc.info.uri) + }) || ( + includeGeneralProperties && (prop.domain.isEmpty || prop.domain.get.info.uri == "http://www.w3.org/2002/07/owl#Thing") + ) + }) + val backwardProperties = if(addBackwardRelations) filterProperties((prop, classes) => prop.range.exists(vc => classes.contains(vc.info.uri))) else Seq.empty (forwardProperties, backwardProperties) } @@ -346,7 +435,7 @@ class TargetVocabularyApi @Inject() () extends InjectedController with UserCont classUri: String): Action[AnyContent] = UserContextAction { implicit userContext => implicit val project: Project = getProject(projectName) // Filter only object properties - val (forwardProperties, backwardProperties) = vocabularyPropertiesByType(taskName, project, classUri, addBackwardRelations = true) + val (forwardProperties, backwardProperties) = vocabularyPropertiesByType(taskName, project, fullClassUri(classUri, project.config.prefixes), addBackwardRelations = true) val forwardObjectProperties = forwardProperties.filter(vp => vp.range.isDefined && vp.domain.isDefined) val f = forwardObjectProperties map (fp => vocabularyPropertyToRelation(fp, forward = true)) val b = backwardProperties map (bp => vocabularyPropertyToRelation(bp, forward = false)) diff --git a/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutoCompletionRequest.scala b/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutoCompletionRequest.scala index 4262080d81..2484b0dd6c 100644 --- a/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutoCompletionRequest.scala +++ b/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutoCompletionRequest.scala @@ -8,16 +8,22 @@ import play.api.libs.json.{Format, Json} /** * Request payload for partial source path auto-completion, i.e. suggest replacements for only parts of a more complex source path. * - * @param inputString The currently entered source path string. - * @param cursorPosition The cursor position inside the source path string. - * @param maxSuggestions The max. number of suggestions to return. - * @param isObjectPath Set to true if the auto-completion results are meant for an object path. Some suggestions might be filtered out or added. + * @param inputString The currently entered source path string. + * @param cursorPosition The cursor position inside the source path string. + * @param maxSuggestions The max. number of suggestions to return. + * @param isObjectPath Set to true if the auto-completion results are meant for an object path. Some suggestions might be filtered out or added. + * @param baseSourcePath The base source path of the auto-completion request. This can be used as alternative to the source path of the rule. + * @param oneHopOnly Return only paths as replacements that have one hop. Default: false + * @param ignorePathOperatorCompletions If there should be NO completions of path operators returned, e.g. '/' etc. be suggested. Default: false */ case class PartialSourcePathAutoCompletionRequest(inputString: String, cursorPosition: Int, maxSuggestions: Option[Int], isObjectPath: Option[Boolean], - taskContext: Option[WorkflowTaskContext]) extends AutoSuggestAutoCompletionRequest { + taskContext: Option[WorkflowTaskContext], + baseSourcePath: Option[String] = None, + oneHopOnly: Option[Boolean] = None, + ignorePathOperatorCompletions: Option[Boolean] = None) extends AutoSuggestAutoCompletionRequest { private val operatorStartChars = Set('/', '\\', '[') /** The remaining characters from the cursor position to the end of the current path operator. */ diff --git a/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutocompletionHelper.scala b/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutocompletionHelper.scala index 7782fdda59..3b68e2157f 100644 --- a/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutocompletionHelper.scala +++ b/silk-workbench/silk-workbench-rules/app/controllers/transform/autoCompletion/PartialSourcePathAutocompletionHelper.scala @@ -141,8 +141,10 @@ object PartialSourcePathAutocompletionHelper { def completion(predicate: Boolean, value: String, description: String): Option[CompletionBase] = { if(predicate) Some(CompletionBase(value, description = Some(description))) else None } + val operatorComplextionsRequested = !autoCompletionRequest.ignorePathOperatorCompletions.getOrElse(false) // Propose operators - if (!pathToReplace.insideQuotesOrUri && !autoCompletionRequest.charBeforeCursor.contains('/') && !autoCompletionRequest.charBeforeCursor.contains('\\')) { + if (operatorComplextionsRequested && !pathToReplace.insideQuotesOrUri && + !autoCompletionRequest.charBeforeCursor.contains('/') && !autoCompletionRequest.charBeforeCursor.contains('\\')) { val supportedPathExpressions = dataSourceCharacteristicsOpt.getOrElse(DatasetCharacteristics()).supportedPathExpressions val forwardOp = completion(autoCompletionRequest.cursorPosition > 0 && supportedPathExpressions.multiHopPaths && !pathToReplace.insideFilter, "/", "Starts a forward path segment") diff --git a/silk-workbench/silk-workbench-rules/app/controllers/transform/transformTask/TransformUtils.scala b/silk-workbench/silk-workbench-rules/app/controllers/transform/transformTask/TransformUtils.scala index 468765e0fb..69cda9ccab 100644 --- a/silk-workbench/silk-workbench-rules/app/controllers/transform/transformTask/TransformUtils.scala +++ b/silk-workbench/silk-workbench-rules/app/controllers/transform/transformTask/TransformUtils.scala @@ -5,20 +5,57 @@ import org.silkframework.dataset.DatasetSpec.GenericDatasetSpec import org.silkframework.dataset.rdf.RdfDataset import org.silkframework.rule.{DatasetSelection, TransformSpec} import org.silkframework.runtime.activity.UserContext +import org.silkframework.util.Identifier import org.silkframework.workspace.activity.dataset.DatasetUtils import org.silkframework.workspace.{Project, ProjectTask} /** Utility functions for transform tasks. */ object TransformUtils { - /** Returns the dataset characteristics if the input task of the transformation is a dataset. */ + /** Returns the dataset characteristics of the input task of the transformation is a dataset. */ def datasetCharacteristics(task: ProjectTask[TransformSpec]) (implicit userContext: UserContext): Option[DatasetCharacteristics] = { - DatasetUtils.datasetCharacteristics(task.project, task.selection) + datasetCharacteristics(task.project, task.selection) } + /** Returns the dataset characteristics of the dataset selection. */ + def datasetCharacteristics(project: Project, + datasetSelection: DatasetSelection) + (implicit userContext: UserContext): Option[DatasetCharacteristics] = { + DatasetUtils.datasetCharacteristics(project, datasetSelection) + } + + /** Returns true if the dataset selection is a RDF dataset. */ + def isRdfDataset(project: Project, + datasetSelection: DatasetSelection) + (implicit userContext: UserContext): Boolean = { + isRdfDatasetById(project, datasetSelection.inputId) + } + + private def isRdfDatasetById(project: Project, + datasetId: Identifier) + (implicit userContext: UserContext): Boolean = { + project.taskOption[GenericDatasetSpec](datasetId).exists(_.data.plugin.isInstanceOf[RdfDataset]) + } + + /** Returns the dataset characteristics of the dataset selection. */ + def outputDatasetCharacteristics(transformTask: ProjectTask[TransformSpec]) + (implicit userContext: UserContext): Option[DatasetCharacteristics] = { + transformTask.output.value.flatMap(output => datasetCharacteristics(transformTask.project, DatasetSelection(output))) + } + + /** Returns true if the configured input task is a RDF dataset. */ def isRdfInput(transformTask: ProjectTask[TransformSpec]) (implicit userContext: UserContext): Boolean = { DatasetUtils.isRdfInput(transformTask.project, transformTask.selection) - transformTask.project.taskOption[GenericDatasetSpec](transformTask.selection.inputId).exists(_.data.plugin.isInstanceOf[RdfDataset]) + } + + /** Returns true if the configured output task is a RDF dataset. */ + def isRdfOutput(transformTask: ProjectTask[TransformSpec]) + (implicit userContext: UserContext): Boolean = { + transformTask.output.value match { + case Some(output) => + isRdfDatasetById(transformTask.project, output) + case None => false + } } } diff --git a/silk-workbench/silk-workbench-rules/app/controllers/transform/vocabulary/SourcePropertiesOfClassRequest.scala b/silk-workbench/silk-workbench-rules/app/controllers/transform/vocabulary/SourcePropertiesOfClassRequest.scala new file mode 100644 index 0000000000..3154b1cb45 --- /dev/null +++ b/silk-workbench/silk-workbench-rules/app/controllers/transform/vocabulary/SourcePropertiesOfClassRequest.scala @@ -0,0 +1,17 @@ +package controllers.transform.vocabulary + +import play.api.libs.json.{Format, Json} + +/** Request for the properties of class for a RDF source. + * + * @param classUri The class URI the properties should be fetched for. + * @param fromPathCacheOnly If true, then only properties are returned that were also found in the paths cache for that RDF source. + * @param includeGeneralProperties If true then also properties defined on owl:Thing and properties without any domain statement are returned. + */ +case class SourcePropertiesOfClassRequest(classUri: String, + fromPathCacheOnly: Boolean, + includeGeneralProperties: Boolean) + +object SourcePropertiesOfClassRequest { + implicit val sourcePropertiesOfClassRequestFormat: Format[SourcePropertiesOfClassRequest] = Json.format[SourcePropertiesOfClassRequest] +} \ No newline at end of file diff --git a/silk-workbench/silk-workbench-rules/conf/transform.routes b/silk-workbench/silk-workbench-rules/conf/transform.routes index 2b4f658fca..d07afcb859 100644 --- a/silk-workbench/silk-workbench-rules/conf/transform.routes +++ b/silk-workbench/silk-workbench-rules/conf/transform.routes @@ -46,9 +46,12 @@ GET /tasks/:project/:task/targetVocabulary/vocabularies GET /tasks/:project/:task/targetVocabulary/type controllers.transform.TargetVocabularyApi.getTypeInfo(project: String, task: String, uri: String) GET /tasks/:project/:task/targetVocabulary/property controllers.transform.TargetVocabularyApi.getPropertyInfo(project: String, task: String, uri: String) GET /tasks/:project/:task/targetVocabulary/typeOrProperty controllers.transform.TargetVocabularyApi.getTypeOrPropertyInfo(project: String, task: String, uri: String) -GET /tasks/:project/:task/targetVocabulary/propertiesByClass controllers.transform.TargetVocabularyApi.propertiesByType(project: String, task: String, classUri: String) +GET /tasks/:project/:task/targetVocabulary/propertiesByClass controllers.transform.TargetVocabularyApi.propertiesByType(project: String, task: String, classUri: String, includeGeneralProperties: Boolean ?= false) GET /tasks/:project/:task/targetVocabulary/relationsOfClass controllers.transform.TargetVocabularyApi.relationsOfType(project: String, task: String, classUri: String) +# Endpoints for RDF source datasets +POST /tasks/:project/:task/sourceVocabulary/propertiesByClass controllers.transform.TargetVocabularyApi.sourcePropertiesByType(project: String, task: String) + POST /tasks/:project/:task/transformInput controllers.transform.TransformTaskApi.postTransformInput(project: String, task: String) GET /tasks/:project/:task/rule/:rule/valueSourcePaths controllers.transform.SourcePathsApi.valueSourcePaths(project: String, task: String, rule: String, maxDepth: Int ?= Int.MaxValue, unusedOnly: Boolean ?= false, usedOnly: Boolean ?= false) GET /tasks/:project/:task/rule/:rule/valueSourcePathsInfo controllers.transform.SourcePathsApi.valueSourcePathsFullInfo(project: String, task: String, rule: String, maxDepth: Int ?= Int.MaxValue, objectInfo: Boolean ?= false) diff --git a/silk-workspace/src/main/scala/org/silkframework/workspace/CombinedWorkspaceProvider.scala b/silk-workspace/src/main/scala/org/silkframework/workspace/CombinedWorkspaceProvider.scala index a8078091bb..74340e2d9c 100644 --- a/silk-workspace/src/main/scala/org/silkframework/workspace/CombinedWorkspaceProvider.scala +++ b/silk-workspace/src/main/scala/org/silkframework/workspace/CombinedWorkspaceProvider.scala @@ -1,7 +1,7 @@ package org.silkframework.workspace import org.silkframework.config.{Tag, Task, TaskSpec} -import org.silkframework.dataset.rdf.SparqlEndpoint +import org.silkframework.dataset.rdf.{GraphStoreTrait, SparqlEndpoint} import org.silkframework.runtime.activity.UserContext import org.silkframework.runtime.plugin.PluginContext import org.silkframework.runtime.resource.ResourceManager @@ -27,7 +27,7 @@ class CombinedWorkspaceProvider(val primaryWorkspace: WorkspaceProvider, private val log = Logger.getLogger(getClass.getName) - override def sparqlEndpoint: Option[SparqlEndpoint] = primaryWorkspace.sparqlEndpoint.orElse(secondaryWorkspace.sparqlEndpoint) + override def sparqlEndpoint: Option[SparqlEndpoint with GraphStoreTrait] = primaryWorkspace.sparqlEndpoint.orElse(secondaryWorkspace.sparqlEndpoint) /** * Refreshes all projects, i.e. cleans all possible caches if there are any and reloads all projects freshly. diff --git a/silk-workspace/src/main/scala/org/silkframework/workspace/InMemoryWorkspaceProvider.scala b/silk-workspace/src/main/scala/org/silkframework/workspace/InMemoryWorkspaceProvider.scala index aeaf914346..d951a50ba4 100644 --- a/silk-workspace/src/main/scala/org/silkframework/workspace/InMemoryWorkspaceProvider.scala +++ b/silk-workspace/src/main/scala/org/silkframework/workspace/InMemoryWorkspaceProvider.scala @@ -2,7 +2,7 @@ package org.silkframework.workspace import org.silkframework.config._ import org.silkframework.dataset.DatasetSpec.GenericDatasetSpec -import org.silkframework.dataset.rdf.SparqlEndpoint +import org.silkframework.dataset.rdf.{GraphStoreTrait, SparqlEndpoint} import org.silkframework.dataset.{Dataset, DatasetSpec} import org.silkframework.runtime.activity.UserContext import org.silkframework.runtime.plugin.annotations.Plugin @@ -237,5 +237,5 @@ class InMemoryWorkspaceProvider() extends WorkspaceProvider { /** * Returns None, because the projects are not held as RDF. */ - override def sparqlEndpoint: Option[SparqlEndpoint] = None + override def sparqlEndpoint: Option[SparqlEndpoint with GraphStoreTrait] = None } diff --git a/silk-workspace/src/main/scala/org/silkframework/workspace/Project.scala b/silk-workspace/src/main/scala/org/silkframework/workspace/Project.scala index ee9565e9f5..ae77e2c333 100644 --- a/silk-workspace/src/main/scala/org/silkframework/workspace/Project.scala +++ b/silk-workspace/src/main/scala/org/silkframework/workspace/Project.scala @@ -391,4 +391,6 @@ class Project(initialConfig: ProjectConfig, provider: WorkspaceProvider, val res override def tags()(implicit userContext: UserContext): Set[Tag] = { config.metaData.tags.map(uri => tagManager.getTag(uri)) } + + override def toString: String = s"Project ${config.labelAndId(config.prefixes)}" } \ No newline at end of file diff --git a/silk-workspace/src/main/scala/org/silkframework/workspace/activity/WorkspaceActivity.scala b/silk-workspace/src/main/scala/org/silkframework/workspace/activity/WorkspaceActivity.scala index 420abccbd9..70f9ddf038 100644 --- a/silk-workspace/src/main/scala/org/silkframework/workspace/activity/WorkspaceActivity.scala +++ b/silk-workspace/src/main/scala/org/silkframework/workspace/activity/WorkspaceActivity.scala @@ -4,7 +4,7 @@ import org.silkframework.config.{DefaultConfig, TaskSpec} import org.silkframework.runtime.activity._ import org.silkframework.runtime.plugin.{ClassPluginDescription, ParameterValues, PluginContext} import org.silkframework.runtime.validation.ServiceUnavailableException -import org.silkframework.util.{Identifier, IdentifierGenerator} +import org.silkframework.util.{Identifier, IdentifierGenerator, PrefixedIdentifierGenerator} import org.silkframework.workspace.{Project, ProjectTask} import java.time.Instant @@ -21,7 +21,7 @@ abstract class WorkspaceActivity[ActivityType <: HasValue : ClassTag]() { /** * Generates new identifiers for created activity instances. */ - private val identifierGenerator = new IdentifierGenerator(name) + private val identifierGenerator = new PrefixedIdentifierGenerator(name) private val log: Logger = Logger.getLogger(this.getClass.getName) lazy private val maxConcurrentExecutionsPerActivity = WorkspaceActivity.maxConcurrentExecutionsPerActivity() diff --git a/silk-workspace/src/main/scala/org/silkframework/workspace/xml/XmlWorkspaceProvider.scala b/silk-workspace/src/main/scala/org/silkframework/workspace/xml/XmlWorkspaceProvider.scala index fdd7867957..a77e921625 100644 --- a/silk-workspace/src/main/scala/org/silkframework/workspace/xml/XmlWorkspaceProvider.scala +++ b/silk-workspace/src/main/scala/org/silkframework/workspace/xml/XmlWorkspaceProvider.scala @@ -2,7 +2,7 @@ package org.silkframework.workspace.xml import org.silkframework.config.Tag.TagXmlFormat import org.silkframework.config._ -import org.silkframework.dataset.rdf.SparqlEndpoint +import org.silkframework.dataset.rdf.{GraphStoreTrait, SparqlEndpoint} import org.silkframework.runtime.activity.UserContext import org.silkframework.runtime.plugin.PluginContext import org.silkframework.runtime.resource.{EmptyResourceManager, ResourceManager} @@ -201,5 +201,5 @@ class XmlWorkspaceProvider(val resources: ResourceManager) extends WorkspaceProv /** * Returns None, because the projects are not held as RDF. */ - override def sparqlEndpoint: Option[SparqlEndpoint] = None + override def sparqlEndpoint: Option[SparqlEndpoint with GraphStoreTrait] = None } diff --git a/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala b/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala index 37ebd7d1d0..72c2e75d14 100644 --- a/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala +++ b/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala @@ -132,7 +132,8 @@ trait WorkspaceProviderTestTrait extends AnyFlatSpec with Matchers with MockitoS DirectMapping( id = TRANSFORM_ID, sourcePath = UntypedPath("prop1"), - metaData = MetaData(Some("Direct Rule Label"), Some("Direct Rule Description")) + metaData = MetaData(Some("Direct Rule Label"), Some("Direct Rule Description")), + inputId = Some("prop1") ), ComplexMapping( id = "complexId", @@ -182,7 +183,8 @@ trait WorkspaceProviderTestTrait extends AnyFlatSpec with Matchers with MockitoS MappingRules(DirectMapping( id = TRANSFORM_ID + 2, sourcePath = UntypedPath("prop5"), - metaData = MetaData(Some("Direct Rule New Label"), Some("Direct Rule New Description")) + metaData = MetaData(Some("Direct Rule New Label"), Some("Direct Rule New Description")), + inputId = Some("prop5") )), mappingTarget = transformTask.data.mappingRule.mappingTarget.copy(isAttribute = true), metaData = MetaData(Some("Root Rule New Label"), Some("Root Rule New Description")) @@ -203,7 +205,7 @@ trait WorkspaceProviderTestTrait extends AnyFlatSpec with Matchers with MockitoS uriRule = None, typeRules = Seq(TypeMapping(typeUri = "Person", metaData = MetaData(Some("type")))), propertyRules = Seq( - DirectMapping("name", sourcePath = UntypedPath("name"), mappingTarget = MappingTarget("name"), MetaData(Some("name"))), + DirectMapping("name", sourcePath = UntypedPath("name"), mappingTarget = MappingTarget("name"), MetaData(Some("name")), Some("name")), ObjectMapping( sourcePath = UntypedPath.empty, target = Some(MappingTarget("address")), @@ -211,8 +213,8 @@ trait WorkspaceProviderTestTrait extends AnyFlatSpec with Matchers with MockitoS uriRule = Some(PatternUriMapping(pattern = s"https://silkframework.org/ex/Address_{city}_{country}", metaData = MetaData(Some("uri")))), typeRules = Seq.empty, propertyRules = Seq( - DirectMapping("city", sourcePath = UntypedPath("city"), mappingTarget = MappingTarget("city"), MetaData(Some("city"))), - DirectMapping("country", sourcePath = UntypedPath("country"), mappingTarget = MappingTarget("country"), MetaData(Some("country"))) + DirectMapping("city", sourcePath = UntypedPath("city"), mappingTarget = MappingTarget("city"), MetaData(Some("city")), Some("city")), + DirectMapping("country", sourcePath = UntypedPath("country"), mappingTarget = MappingTarget("country"), MetaData(Some("country")), Some("city")) ) ), metaData = MetaData(Some("object")) diff --git a/workspace/package.json b/workspace/package.json index 46d097d672..918c0b2495 100644 --- a/workspace/package.json +++ b/workspace/package.json @@ -175,7 +175,7 @@ "@testing-library/jest-dom": "^5.11.0", "@testing-library/react": "^10.4.3", "@testing-library/user-event": "^12.0.11", - "@types/carbon-components-react": "7.42.0", + "@types/carbon-components-react": "7.55.2", "@types/codemirror": "^5.60.15", "@types/core-js": "^2.5.3", "@types/enzyme": "^3.9.1", diff --git a/workspace/src/app/store/ducks/workspace/requests.ts b/workspace/src/app/store/ducks/workspace/requests.ts index ce58a23b15..c1bf131d8f 100644 --- a/workspace/src/app/store/ducks/workspace/requests.ts +++ b/workspace/src/app/store/ducks/workspace/requests.ts @@ -168,7 +168,7 @@ export const requestCreateProject = async (payload: ICreateProjectPayload): Prom }; //missing-type -export const requestProjectPrefixes = async (projectId: string): Promise => { +export const requestProjectPrefixesLegacy = async (projectId: string): Promise => { try { const { data } = await fetch({ url: workspaceApi(`/projects/${projectId}/prefixes`), @@ -179,6 +179,13 @@ export const requestProjectPrefixes = async (projectId: string): Promise>> => { + return fetch({ + url: workspaceApi(`/projects/${projectId}/prefixes`), + }) +}; + //missing-type export const requestChangePrefixes = async ( prefixName: string, diff --git a/workspace/src/app/store/ducks/workspace/widgets/configuration.thunk.ts b/workspace/src/app/store/ducks/workspace/widgets/configuration.thunk.ts index 835f137a8a..bb720e511b 100644 --- a/workspace/src/app/store/ducks/workspace/widgets/configuration.thunk.ts +++ b/workspace/src/app/store/ducks/workspace/widgets/configuration.thunk.ts @@ -1,6 +1,6 @@ import { widgetsSlice } from "@ducks/workspace/widgetsSlice"; import { batch } from "react-redux"; -import { requestChangePrefixes, requestProjectPrefixes, requestRemoveProjectPrefix } from "@ducks/workspace/requests"; +import { requestChangePrefixes, requestProjectPrefixesLegacy, requestRemoveProjectPrefix } from "@ducks/workspace/requests"; const { setPrefixes, resetNewPrefix, toggleWidgetLoading, setWidgetError } = widgetsSlice.actions; @@ -35,7 +35,7 @@ export const fetchProjectPrefixesAsync = (projectId: string) => { return async (dispatch) => { try { dispatch(toggleLoading()); - const data = await requestProjectPrefixes(projectId); + const data = await requestProjectPrefixesLegacy(projectId); dispatch(updatePrefixList(data)); } catch (e) { dispatch(setError(e)); diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/HierarchicalMapping.jsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/HierarchicalMapping.jsx index 20d297f864..de46a9b905 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/HierarchicalMapping.jsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/HierarchicalMapping.jsx @@ -240,7 +240,8 @@ class HierarchicalMapping extends React.Component { value={{ valueTypeLabels: this.state.valueTypeLabels, taskContext: this.props.viewActions.taskContext?.context, - transformTask: this.props.transformTask, + projectId: this.props.project, + transformTaskId: this.props.transformTask, }} >
diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/components/AutoComplete.tsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/components/AutoComplete.tsx index 7e7d3765ca..c3c1411c6e 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/components/AutoComplete.tsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/components/AutoComplete.tsx @@ -109,9 +109,9 @@ interface IAutoCompleteItem { const hasDistinctLabel = (autoCompleteItem: IAutoCompleteItem) => autoCompleteItem.label && autoCompleteItem.label.toLowerCase() !== autoCompleteItem.value.toLowerCase(); -const autoCompleteItemRendererFactory = (showValueWhenLabelExists: boolean) => { +export function autoCompleteItemRendererFactory(showValueWhenLabelExists: boolean, optionalLabelFn?: (obj: T) => string) { return ( - autoCompleteItem: IAutoCompleteItem, + autoCompleteItem: IAutoCompleteItem & T, query: string, modifiers: SuggestFieldItemRendererModifierProps, handleClick: () => any @@ -125,6 +125,9 @@ const autoCompleteItemRendererFactory = (showValueWhenLabelExists: boolean) => { } else { label = autoCompleteItem.value; } + if(optionalLabelFn) { + label = optionalLabelFn(autoCompleteItem) + } const highlighter = (value: string | undefined) => modifiers.highlightingEnabled ? : value; const item = diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingHeader.jsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingHeader.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ObjectRule/ObjectRule.jsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ObjectRule/ObjectRule.jsx index 3e235ae2e2..1859b3244e 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ObjectRule/ObjectRule.jsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ObjectRule/ObjectRule.jsx @@ -226,7 +226,9 @@ class ObjectRule extends React.Component { ) : null} - + {isCopiableRule(ruleType) && } {isClonableRule(ruleType) && } diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRule.jsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRule.jsx index c72461088a..3d46ceb6fa 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRule.jsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRule.jsx @@ -153,7 +153,9 @@ class ValueRule extends React.Component { ) : null} - + diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRuleForm.tsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRuleForm.tsx index 47044ee807..6983851e71 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRuleForm.tsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingRule/ValueRule/ValueRuleForm.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { Card, CardActions, CardContent, CardTitle, ScrollingHOC } from "gui-elements-deprecated"; +import { debounce } from "lodash"; import { AffirmativeButton, DismissiveButton, @@ -129,6 +130,17 @@ export function ValueRuleForm(props: IProps) { }); }, []); + // Delay a bit so direct user interactions are not disturbed by re-renderings + const changeValuePathInputHasFocus = React.useCallback( + debounce( + (hasFocus: boolean) => { + setValuePathInputHasFocus(hasFocus) + }, + 200 + ), + [] + ) + const autoCompleteRuleId = id || parentId; const state = { @@ -406,7 +418,7 @@ export function ValueRuleForm(props: IProps) { } checkInput={checkValuePathValidity} onInputChecked={setValuePathValid} - onFocusChange={setValuePathInputHasFocus} + onFocusChange={changeValuePathInputHasFocus} rightElement={} /> diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingsWorkview.tsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingsWorkview.tsx index a3ff5cd80d..b498730ec6 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingsWorkview.tsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/MappingsWorkview.tsx @@ -22,6 +22,7 @@ import { import EventEmitter from "../utils/EventEmitter"; import { diErrorMessage } from "@ducks/error/typings"; import { IViewActions } from "../../../../plugins/PluginRegistry"; +import { SuggestionNGProps } from "../../../../plugins/plugin.types"; import { ParentStructure } from "../components/ParentStructure"; import RuleTitle from "../elements/RuleTitle"; diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/InfoBoxOverlay.tsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/InfoBoxOverlay.tsx index 389365d8eb..c5566ab101 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/InfoBoxOverlay.tsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/InfoBoxOverlay.tsx @@ -21,69 +21,69 @@ interface IDataStackItem { interface IDataStack extends TestableComponent { data: IDataStackItem[]; + /** If true the content will be returned directly instead being shown as context overlay. */ + embed?: boolean } -export function InfoBoxOverlay({ data, ...otherProps }: IDataStack) { +export function InfoBoxOverlay({data, embed = false,...otherProps}: IDataStack) { const { portalContainer } = useContext(SuggestionListContext); - const dataTestId = (suffix: string) => - otherProps["data-test-id"] ? otherProps["data-test-id"] + suffix : undefined; - - return ( - - - - - - - - + const dataTestId = (suffix: string) => otherProps["data-test-id"] ? otherProps["data-test-id"] + suffix : undefined + const Content = () => + +
+ + + + + {data.map((item, idx) => item.value ? ( - - + + - - - {item.value} - + + + + {item.value} + ) : null )} - -
-
- - } + + + + + + return embed? + : + } > - + - ); } diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/SourcePathInfoBox.tsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/SourcePathInfoBox.tsx index c962865ab8..2b45a49ba0 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/SourcePathInfoBox.tsx +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/containers/SuggestionNew/SuggestionTable/StableRow/SourcePathInfoBox.tsx @@ -3,6 +3,7 @@ import { HtmlContentBlock } from "@eccenca/gui-elements"; import {ITargetWithSelected, SuggestionTypeValues} from "../../suggestion.typings"; import { SuggestionListContext } from "../../SuggestionContainer"; import { InfoBoxOverlay } from "./InfoBoxOverlay"; +import {useTranslation} from "react-i18next"; interface IProps { source?: string | ITargetWithSelected[]; @@ -11,24 +12,29 @@ interface IProps { dataTypeSubPaths: string[] objectSubPaths: string[] } + embed?: boolean + exampleValues?: string[] } /** Shows additional information for a dataset source path, e.g. examples values. */ -export function SourcePathInfoBox({source, pathType, objectInfo}: IProps) { +export function SourcePathInfoBox({source, pathType, objectInfo, embed, exampleValues}: IProps) { const context = useContext(SuggestionListContext); - const {exampleValues} = context; + const {exampleValues: contextExampleValues} = context; + const [t] = useTranslation() let examples: string[] | undefined = []; let sourcePath: string = "" - if (typeof source === 'string') { + if(exampleValues) { + examples = exampleValues + } else if (typeof source === 'string') { sourcePath = source - examples = exampleValues[source as string]; + examples = contextExampleValues[source as string]; } else if (Array.isArray(source)) { // There is always one item selected from the target list const selected = source.find(t => t._selected) as ITargetWithSelected; sourcePath = selected.uri - if (selected && exampleValues[selected.uri]) { - examples = exampleValues[selected.uri] + if (selected && contextExampleValues[selected.uri]) { + examples = contextExampleValues[selected.uri] } } @@ -36,14 +42,14 @@ export function SourcePathInfoBox({source, pathType, objectInfo}: IProps) { const infoBoxProperties = [ { - key: "Source path", + key: t("MappingSuggestion.SourceElement.infobox.sourcePath"), value: simpleStringValue(sourcePath) } ] if(examples && examples.length > 0) { infoBoxProperties.push({ - key: "Example data", + key: t("MappingSuggestion.SourceElement.infobox.exampleData"), value: , @@ -53,19 +59,21 @@ export function SourcePathInfoBox({source, pathType, objectInfo}: IProps) { if(pathType) { infoBoxProperties.push({ key: "Path type", - value: simpleStringValue(pathType === "object" ? "Object path" : "Value path") + value: simpleStringValue(pathType === "object" ? + t("MappingSuggestion.SourceElement.infobox.objectPath") : + t("MappingSuggestion.SourceElement.infobox.valuePath")) }) } if(objectInfo) { infoBoxProperties.push({ - key: `Data type sub-paths (${objectInfo.dataTypeSubPaths.length})`, + key: `${t("MappingSuggestion.SourceElement.infobox.dataTypeSubPaths")} (${objectInfo.dataTypeSubPaths.length})`, value:
}) infoBoxProperties.push({ - key: `Object type sub-paths (${objectInfo.objectSubPaths.length})`, + key: `${t("MappingSuggestion.SourceElement.infobox.objectTypeSubPaths")} (${objectInfo.objectSubPaths.length})`, value:
@@ -75,6 +83,7 @@ export function SourcePathInfoBox({source, pathType, objectInfo}: IProps) { return ; } diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/store.ts b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/store.ts index c5ab6eb5f5..7e9207d086 100644 --- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/store.ts +++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/store.ts @@ -723,7 +723,7 @@ const getValuePathSuggestion = ( cursorPosition: number, isObjectPath: boolean, taskContext?: TaskContext -): HttpResponsePromise => { +): HttpResponsePromise => { const { transformTask, project } = getDefinedApiDetails(); return silkApi.getSuggestionsForAutoCompletion( project, diff --git a/workspace/src/app/views/pages/MappingEditor/api/silkRestApi.ts b/workspace/src/app/views/pages/MappingEditor/api/silkRestApi.ts index bda2f7125a..c30ddd44ab 100644 --- a/workspace/src/app/views/pages/MappingEditor/api/silkRestApi.ts +++ b/workspace/src/app/views/pages/MappingEditor/api/silkRestApi.ts @@ -1,8 +1,9 @@ import superagent from '@eccenca/superagent'; import Promise from 'bluebird'; -import {IUriPatternsResult} from "./types"; +import {IUriPatternsResult, PropertyByDomainAutoCompletion, TargetPropertyAutoCompletion} from "./types"; import {CONTEXT_PATH} from "../../../../constants/path"; import {TaskContext} from "../../../shared/projectTaskTabView/projectTaskTabView.typing"; +import {IPartialAutoCompleteResult} from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion" const CONTENT_TYPE_JSON = 'application/json'; @@ -156,8 +157,8 @@ const silkApi = { /** Retrieves target properties that are valid for the specific transform rule as target property. */ retrieveTransformTargetProperties: function(projectId: string, taskId: string, ruleId: string, - searchTerm?: string, maxResults: number = 30, vocabularies?: string[], - fullUris: boolean = true, taskContext?: TaskContext): HttpResponsePromise { + searchTerm?: string, maxResults: number = 30, vocabularies?: string[] | undefined, + fullUris: boolean = true, taskContext?: TaskContext): HttpResponsePromise { const requestUrl = this.transformTargetPropertyEndpoint(projectId, taskId, ruleId, searchTerm, maxResults, fullUris); const promise = superagent @@ -172,6 +173,37 @@ const silkApi = { return this.handleErrorCode(promise); }, + /** Retrieves target property auto-completions. */ + targetClassAutoCompletions: function (projectId: string, + taskId: string, + searchTerm: string | undefined, + maxResults: number): HttpResponsePromise { + const requestUrl = this.transformTargetTypesEndpoint(projectId, taskId, "notUsedInBackend", searchTerm, maxResults); + + const promise = superagent + .get(requestUrl) + .accept(CONTENT_TYPE_JSON) + + return this.handleErrorCode(promise); + }, + + propertiesByClass: function (projectId: string, + transformTaskId: string, + classUri: string, + includeGeneralProperties?: boolean): HttpResponsePromise { + const requestUrl = this.propertiesByTypeEndpoint(projectId, transformTaskId) + + return this.handleErrorCode( + superagent + .get(requestUrl) + .accept(CONTENT_TYPE_JSON) + .query({ + classUri, + includeGeneralProperties + }) + ) + }, + /** * Requests auto-completion suggestions for the script task code. */ @@ -295,14 +327,36 @@ const silkApi = { return `${CONTEXT_PATH}/transform/tasks/${projectId}/${transformTaskId}/rule/${ruleId}/completions/targetProperties?term=${encodedSearchTerm}&maxResults=${maxResults}&fullUris=${fullUris}` }, + transformTargetTypesEndpoint: function(projectId: string, + transformTaskId: string, + ruleId: string, + searchTerm: string | undefined, + maxResults: number): string { + const encodedSearchTerm = searchTerm ? encodeURIComponent(searchTerm) : "" + return `${CONTEXT_PATH}/transform/tasks/${projectId}/${transformTaskId}/rule/${ruleId}/completions/targetTypes?term=${encodedSearchTerm}&maxResults=${maxResults}` + }, + + propertiesByTypeEndpoint: function(projectId: string, + transformTaskId: string): string { + return `${CONTEXT_PATH}/transform/tasks/${projectId}/${transformTaskId}/targetVocabulary/propertiesByClass` + }, + getSuggestionsForAutoCompletion: function(projectId:string, transformTaskId:string, ruleId:string, inputString:string, cursorPosition: number, isObjectPath: boolean, - taskContext?: TaskContext): HttpResponsePromise { + taskContext?: TaskContext, baseSourcePath?: string, oneHopOnly?: boolean, + ignorePathOperatorCompletions?: boolean): HttpResponsePromise { const requestUrl = `${CONTEXT_PATH}/transform/tasks/${projectId}/${transformTaskId}/rule/${ruleId}/completions/partialSourcePaths`; + const requestBody: any = { inputString, cursorPosition, maxSuggestions: 50, isObjectPath, taskContext, baseSourcePath } + if(oneHopOnly) { + requestBody.oneHopOnly = true + } + if(ignorePathOperatorCompletions) { + requestBody.ignorePathOperatorCompletions = true + } const promise = superagent .post(requestUrl) .set("Content-Type", CONTENT_TYPE_JSON) - .send({ inputString, cursorPosition, maxSuggestions: 50, isObjectPath, taskContext }); + .send(requestBody); return this.handleErrorCode(promise) }, diff --git a/workspace/src/app/views/pages/MappingEditor/api/types.ts b/workspace/src/app/views/pages/MappingEditor/api/types.ts index c8b3bbe202..3d92216608 100644 --- a/workspace/src/app/views/pages/MappingEditor/api/types.ts +++ b/workspace/src/app/views/pages/MappingEditor/api/types.ts @@ -11,4 +11,49 @@ export interface IUriPattern { label: string // The full URI pattern value: string -} \ No newline at end of file +} + +/** Target property auto-completion */ +export interface TargetPropertyAutoCompletion { + /** The URI */ + value: string, + /** label of the property */ + label?: string, + /** description of the property */ + description?: string, + category?: string, + isCompletion?: boolean, + extra: { + /** Object or data type property */ + type: "object" | "value", + graph?: string + /** In case of an object property optionally the class info of the range. */ + range?: { + uri: string, + label?: string + } + } +} + +export interface GenericInfo { + uri: string + label?: string + description?: string + altLabels?: string[] +} + +export interface TargetClassAutoCompletion { + /** The URI */ + value: string, + /** label of the property */ + label?: string, + /** description of the property */ + description?: string +} + +export interface PropertyByDomainAutoCompletion { + domain: string + genericInfo: GenericInfo + propertyType: "ObjectProperty" | "DatatypeProperty" + range: string +} diff --git a/workspace/src/app/views/pages/MappingEditor/contexts/GlobalMappingEditorContext.ts b/workspace/src/app/views/pages/MappingEditor/contexts/GlobalMappingEditorContext.ts index fd2c2b8aff..4436dd140c 100644 --- a/workspace/src/app/views/pages/MappingEditor/contexts/GlobalMappingEditorContext.ts +++ b/workspace/src/app/views/pages/MappingEditor/contexts/GlobalMappingEditorContext.ts @@ -2,6 +2,10 @@ import React from "react"; import {TaskContext} from "../../../shared/projectTaskTabView/projectTaskTabView.typing"; interface GlobalMappingEditorContextProps { + /** The ID of the project. */ + projectId: string + /** The ID of the transform task. */ + transformTaskId: string /** A mapping from value type id to label. */ valueTypeLabels: Map; /** Optional transform task context. */ @@ -11,4 +15,6 @@ interface GlobalMappingEditorContextProps { /** Global properties of a specific mapping editor instance. */ export const GlobalMappingEditorContext = React.createContext({ valueTypeLabels: new Map(), + projectId: "", + transformTaskId: "" }); diff --git a/workspace/src/app/views/plugins/PluginRegistry.tsx b/workspace/src/app/views/plugins/PluginRegistry.tsx index fb3d8c9829..1a85013e30 100644 --- a/workspace/src/app/views/plugins/PluginRegistry.tsx +++ b/workspace/src/app/views/plugins/PluginRegistry.tsx @@ -52,6 +52,12 @@ export interface IProjectTaskView { queryParametersToKeep?: string[]; /** Specifies the task context support for this view, e.g. that it uses the information given with the task context. */ supportsTaskContext?: boolean; + /** + * Used to sort a stack of defined views when ordering is important, e.g. if their tabs are displayed. + * If not given then internally `9999`is used. + * Views are sorted in ascending order. + */ + sortOrder?: number; } /** A plugin component that can receive arbitrary parameters. */ @@ -140,6 +146,7 @@ export const SUPPORTED_PLUGINS = { DI_LANGUAGE_SWITCHER: "di:languageSwitcher", DI_BRANDING: "di:branding", DI_PARAMETER_EXTENSIONS: "di:parameterExtensions", + DI_MATCHING: "di:matchingNG", }; registerCorePlugins(); diff --git a/workspace/src/app/views/plugins/plugin.types.ts b/workspace/src/app/views/plugins/plugin.types.ts index f927d3f7df..a1494d4ede 100644 --- a/workspace/src/app/views/plugins/plugin.types.ts +++ b/workspace/src/app/views/plugins/plugin.types.ts @@ -1,4 +1,5 @@ import { IArtefactItemProperty } from "@ducks/common/typings"; +import { IViewActions } from "../../views/plugins/PluginRegistry"; import { TestableComponent } from "@eccenca/gui-elements"; export type IPreview = IDatasetConfigPreview | IResourcePreview | IDatasetPreview | FixedPreview; @@ -109,6 +110,16 @@ export interface ParameterExtensions { extend: (input: IArtefactItemProperty) => IArtefactItemProperty; } +/** Props for mapping suggestion. */ +export interface SuggestionNGProps { + // Project the transform task is in. + projectId: string; + // The transform task to create matches for. + transformTaskId: string; + // Generic actions and callbacks that could be necessary. + viewActions?: IViewActions; +} + /** The preview gets a fixed list of types and their values. */ export interface FixedPreview { /** The list of possible types. */ diff --git a/workspace/src/app/views/shared/AdvancedOptionsArea/AdvancedOptionsArea.tsx b/workspace/src/app/views/shared/AdvancedOptionsArea/AdvancedOptionsArea.tsx index e836d2287c..5aa6787e79 100644 --- a/workspace/src/app/views/shared/AdvancedOptionsArea/AdvancedOptionsArea.tsx +++ b/workspace/src/app/views/shared/AdvancedOptionsArea/AdvancedOptionsArea.tsx @@ -1,8 +1,14 @@ import React from "react"; import { Accordion, AccordionItem, TitleSubsection } from "@eccenca/gui-elements"; import { useTranslation } from "react-i18next"; +import { compact } from "lodash"; -export function AdvancedOptionsArea({ children, open = false, ...otherProps }: any) { +interface AdvancedOptionsAreaProps { + children: any; + open?: boolean; + compact?: boolean; +} +export function AdvancedOptionsArea({ children, open = false, compact = false }: AdvancedOptionsAreaProps) { const [t] = useTranslation(); return ( @@ -11,6 +17,8 @@ export function AdvancedOptionsArea({ children, open = false, ...otherProps }: a label={{t("common.words.advancedOptions", "Advanced options")}} fullWidth elevated + noBorder={compact} + whitespaceSize={"none"} open={open} > {children} diff --git a/workspace/src/app/views/shared/RuleEditor/RuleEditor.typings.ts b/workspace/src/app/views/shared/RuleEditor/RuleEditor.typings.ts index 16b11d8216..4809e34302 100644 --- a/workspace/src/app/views/shared/RuleEditor/RuleEditor.typings.ts +++ b/workspace/src/app/views/shared/RuleEditor/RuleEditor.typings.ts @@ -90,6 +90,8 @@ export interface IParameterSpecification { distanceMeasureRange?: DistanceMeasureRange; /** some required fields have additional labels to specify what values are acceptable */ requiredLabel?: string; + /** The order index of this parameter. The index in the order of the parameter. First parameter starts with 0. */ + orderIdx: number } export interface IParameterValidationResult { diff --git a/workspace/src/app/views/shared/RuleEditor/model/test/RuleEditorModel.test.tsx b/workspace/src/app/views/shared/RuleEditor/model/test/RuleEditorModel.test.tsx index a639401965..418e8cb03f 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/test/RuleEditorModel.test.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/test/RuleEditorModel.test.tsx @@ -175,6 +175,7 @@ describe("Rule editor model", () => { label: paramId, required: false, type: "textField", + orderIdx: 1 }; }; diff --git a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/NodeContent.tsx b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/NodeContent.tsx index 437c2b92ba..418e13e48a 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/NodeContent.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/NodeContent.tsx @@ -64,17 +64,9 @@ export const NodeContent = ({ parameterSpecification: paramSpec, }; }) - // Required parameters to the top, advanced to the bottom, sort alphabetically + // Sort by order given in the plugin spec .sort((paramA, paramB) => { - return paramA.parameterSpecification.required !== paramB.parameterSpecification.required - ? paramA.parameterSpecification.required - ? -1 - : 1 - : paramA.parameterSpecification.advanced !== paramB.parameterSpecification.advanced - ? paramA.parameterSpecification.advanced - ? 1 - : -1 - : paramA.parameterSpecification.label.toLowerCase() < paramB.parameterSpecification.label.toLowerCase() + return paramA.parameterSpecification.orderIdx < paramB.parameterSpecification.orderIdx ? -1 : 1; }); diff --git a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/RuleNodeParameterForm.tsx b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/RuleNodeParameterForm.tsx index db19e7336e..baf9daf33f 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/RuleNodeParameterForm.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/RuleNodeParameterForm.tsx @@ -44,10 +44,7 @@ export const RuleNodeParameterForm = ({ ); const shownParameters = [...normalParameters]; - if (ruleEditorUiContext.advancedParameterModeEnabled && !hasAdvancedSection) { - shownParameters.push(...advancedParameters); - } - const advancedSectionParameters = hasAdvancedSection ? advancedParameters : []; + const advancedSectionParameters = hasAdvancedSection || ruleEditorUiContext.advancedParameterModeEnabled ? advancedParameters : []; const renderFormParameter = (param: IRuleNodeParameter) => { return ( {shownParameters.map(renderFormParameter)} {advancedSectionParameters.length > 0 ? ( - + {advancedSectionParameters.map(renderFormParameter)} ) : null} diff --git a/workspace/src/app/views/shared/SearchList/SearchTags.tsx b/workspace/src/app/views/shared/SearchList/SearchTags.tsx index f2fe91c84c..dae6da9dd0 100644 --- a/workspace/src/app/views/shared/SearchList/SearchTags.tsx +++ b/workspace/src/app/views/shared/SearchList/SearchTags.tsx @@ -19,11 +19,11 @@ export const SearchTags = ({ searchTags, searchText, withSpacing = "tiny" }: Sea {searchTags.map((searchTag) => ( - + {withSpacing !== "none" ? : null} ))} ); -}; +} diff --git a/workspace/src/app/views/shared/projectTaskTabView/ProjectTaskTabView.tsx b/workspace/src/app/views/shared/projectTaskTabView/ProjectTaskTabView.tsx index 462550f878..45f2aa225d 100644 --- a/workspace/src/app/views/shared/projectTaskTabView/ProjectTaskTabView.tsx +++ b/workspace/src/app/views/shared/projectTaskTabView/ProjectTaskTabView.tsx @@ -29,6 +29,7 @@ import "./projectTaskTabView.scss"; import { IProjectTaskView, IViewActions, pluginRegistry } from "../../plugins/PluginRegistry"; import PromptModal from "./PromptModal"; import ErrorBoundary from "../../../ErrorBoundary"; +import {ProjectTaskTabViewContext} from "./ProjectTaskTabViewContext"; const getBookmark = () => window.location.pathname.split("/").slice(-1)[0]; @@ -144,7 +145,10 @@ export function ProjectTaskTabView({ React.useEffect(() => { if (projectId && taskId) { if (taskViewConfig?.pluginId) { - setTaskViews(pluginRegistry.taskViews(taskViewConfig.pluginId)); + const taskViewPlugins = pluginRegistry.taskViews(taskViewConfig.pluginId).sort((a, b) => { + return (a.sortOrder ?? 9999) - (b.sortOrder ?? 9999); + }); + setTaskViews(taskViewPlugins); } else { setTaskViews([]); } @@ -364,7 +368,11 @@ export function ProjectTaskTabView({ getTaskView(selectedTab)?.supportsTaskContext && viewActions?.taskContext ? viewActions.taskContext.taskViewSuffix?.(viewActions.taskContext.context) : undefined; - return ( + return {warnings && } - ); + }; return ( diff --git a/workspace/src/app/views/shared/projectTaskTabView/ProjectTaskTabViewContext.tsx b/workspace/src/app/views/shared/projectTaskTabView/ProjectTaskTabViewContext.tsx new file mode 100644 index 0000000000..f76947d65c --- /dev/null +++ b/workspace/src/app/views/shared/projectTaskTabView/ProjectTaskTabViewContext.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +interface ProjectTaskTabViewContextProps { + fullScreen: boolean +} + +export const ProjectTaskTabViewContext = React.createContext({ + fullScreen: false +}) diff --git a/workspace/src/app/views/taskViews/linking/LinkingRuleEditor.tsx b/workspace/src/app/views/taskViews/linking/LinkingRuleEditor.tsx index 523bc2ef72..439ddee721 100644 --- a/workspace/src/app/views/taskViews/linking/LinkingRuleEditor.tsx +++ b/workspace/src/app/views/taskViews/linking/LinkingRuleEditor.tsx @@ -242,6 +242,7 @@ export const LinkingRuleEditor = ({ projectId, linkingTaskId, viewActions, insta type: "int", advanced: true, defaultValue: "1", + orderIdx: -0.5, }); const thresholdParameterSpec = (pluginDetails: IPluginDetails) => { @@ -290,6 +291,7 @@ export const LinkingRuleEditor = ({ projectId, linkingTaskId, viewActions, insta defaultValue: "0.0", customValidation: customValidation(pluginDetails.distanceMeasureRange), distanceMeasureRange: pluginDetails.distanceMeasureRange, + orderIdx: -1, }); }; diff --git a/workspace/src/app/views/taskViews/linking/activeLearning/LinkingRuleActiveLeraningConfig.scss b/workspace/src/app/views/taskViews/linking/activeLearning/LinkingRuleActiveLeraningConfig.scss index f122794ab2..3dfad8636f 100644 --- a/workspace/src/app/views/taskViews/linking/activeLearning/LinkingRuleActiveLeraningConfig.scss +++ b/workspace/src/app/views/taskViews/linking/activeLearning/LinkingRuleActiveLeraningConfig.scss @@ -6,12 +6,6 @@ $diapp-entitybox-cell-background-even: $eccgui-color-tablerow-background-even !d $diapp-entitybox-cell-background-hovered: $eccgui-color-tablerow-hover !default; $diapp-entitybox-cell-border: $eccgui-color-tablerow-hover !default; -.diapp-linking-learningdata__container { -} - -.diapp-linking-learningdata__row { -} - .diapp-linking-learningdata__header.fullwidth, .diapp-linking-learningdata__cell.fullwidth { max-width: 100%; @@ -23,11 +17,11 @@ $diapp-entitybox-cell-border: $eccgui-color-tablerow-hover !default; border-color: $diapp-entitybox-cell-border; border-width: 2px; border-style: solid solid none solid; - padding: 0.75rem 7px; + padding: $eccgui-size-block-whitespace * 0.75 $eccgui-size-block-whitespace * 0.5; font-weight: 500; color: #000; vertical-align: middle; - font-size: 0.875rem; + font-size: $eccgui-size-typo-caption; } .diapp-linking-learningdata__cell.shrink { @@ -42,7 +36,8 @@ $diapp-entitybox-cell-border: $eccgui-color-tablerow-hover !default; background-color: $diapp-entitybox-cell-background-even; border-width: 2px; border-style: none solid; - padding: 7px 7px 3.5px 7px; + padding: $eccgui-size-block-whitespace * 0.5; + padding-bottom: $eccgui-size-block-whitespace * 0.25; transition: 300ms; visibility: visible; opacity: 1; @@ -117,13 +112,13 @@ div.diapp-linking-connectionenabled__arrow-right { background: currentColor; width: 100%; clip-path: polygon( - 0 7px, - calc(100% - 7px) 7px, - calc(100% - 7px) 0, + 0 #{$eccgui-size-block-whitespace * 0.5}, + calc(100% - #{$eccgui-size-block-whitespace * 0.5}) #{$eccgui-size-block-whitespace * 0.5}, + calc(100% - #{$eccgui-size-block-whitespace * 0.5}) 0, 100% 50%, - calc(100% - 7px) 100%, - calc(100% - 7px) calc(100% - 7px), - 0 9px + calc(100% - #{$eccgui-size-block-whitespace * 0.5}) 100%, + calc(100% - #{$eccgui-size-block-whitespace * 0.5}) calc(100% - #{$eccgui-size-block-whitespace * 0.5}), + 0 #{$eccgui-size-block-whitespace * 0.61} ); } } @@ -135,7 +130,15 @@ div.diapp-linking-connectionenabled__arrow-left { content: ""; background: currentColor; width: 100%; - clip-path: polygon(0 50%, 7px 100%, 7px calc(100% - 7px), 100% calc(100% - 7px), 100% 7px, 7px 7px, 7px 0%); + clip-path: polygon( + 0 50%, + #{$eccgui-size-block-whitespace * 0.5} 100%, + #{$eccgui-size-block-whitespace * 0.5} calc(100% - #{$eccgui-size-block-whitespace * 0.5}), + 100% calc(100% - #{$eccgui-size-block-whitespace * 0.5}), + 100% #{$eccgui-size-block-whitespace * 0.5}, + #{$eccgui-size-block-whitespace * 0.5} #{$eccgui-size-block-whitespace * 0.5}, + #{$eccgui-size-block-whitespace * 0.5} 0% + ); } } diff --git a/workspace/src/app/views/taskViews/shared/rules/rule.utils.tsx b/workspace/src/app/views/taskViews/shared/rules/rule.utils.tsx index cd8e879185..4836186ecc 100644 --- a/workspace/src/app/views/taskViews/shared/rules/rule.utils.tsx +++ b/workspace/src/app/views/taskViews/shared/rules/rule.utils.tsx @@ -183,6 +183,7 @@ const inputPathOperator = ( customItemRenderer: customInputPathRenderer, } : undefined, + orderIdx: 1 }), }, categories: [...additionalCategories, "Recommended"], @@ -206,6 +207,7 @@ const parameterSpecification = ({ autoCompletion, distanceMeasureRange, requiredLabel, + orderIdx }: Omit & Partial>): IParameterSpecification => { return { @@ -219,6 +221,7 @@ const parameterSpecification = ({ autoCompletion, distanceMeasureRange, requiredLabel, + orderIdx }; }; @@ -250,7 +253,7 @@ const convertRuleOperator = ( parameterSpecification: Object.fromEntries([ ...Object.entries(pluginDetails.properties) .filter(([paramId, paramSpec]) => !inputsCanBeSwitched || paramId !== REVERSE_PARAMETER_ID) - .map(([parameterId, parameterSpec]) => { + .map(([parameterId, parameterSpec], idx) => { const spec: IParameterSpecification = { label: parameterSpec.title, description: parameterSpec.description, @@ -259,6 +262,7 @@ const convertRuleOperator = ( type: convertPluginParameterType(parameterSpec.parameterType), autoCompletion: parameterSpec.autoCompletion, defaultValue: optionallyLabelledParameterToValue(parameterSpec.value) ?? "", + orderIdx: idx }; return [parameterId, spec]; }), diff --git a/workspace/src/locales/manual/en.json b/workspace/src/locales/manual/en.json index b1cef18a3a..4f5619fcfa 100644 --- a/workspace/src/locales/manual/en.json +++ b/workspace/src/locales/manual/en.json @@ -345,7 +345,9 @@ "clearInput": "Clear input", "filterOn": "Use as filter", "filterOff": "Remove filter", - "showMoreDetails": "Show more details" + "showMoreDetails": "Show more details", + "restore": "Restore", + "revert": "Revert" }, "app": { "build": "Workbench", @@ -1199,6 +1201,16 @@ "suggestionIssues": { "noFoundClass": "None of the target classes have been found in the target vocabularies. Falling back to matching against all properties instead.", "notAllClassesFound": "Following target classes have not been found in any of the target vocabularies: {{classesString}}.\n\nMatching only against {{numberOfFoundClasses}} of {{numberOfAllClasses}} target classes." + }, + "SourceElement": { + "infobox": { + "exampleData": "Example data", + "sourcePath": "Source path", + "objectPath": "Object path", + "valuePath": "Value path", + "dataTypeSubPaths": "Datatype sub-paths", + "objectTypeSubPaths": "Object type sub-paths" + } } }, "InvisibleCharacterHandling": { diff --git a/workspace/test/integration/TestHelper.tsx b/workspace/test/integration/TestHelper.tsx index 5dbbc7c368..6488ff2d7c 100644 --- a/workspace/test/integration/TestHelper.tsx +++ b/workspace/test/integration/TestHelper.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { createBrowserHistory, createMemoryHistory, History, LocationState } from "history"; -import { EnzymePropSelector, mount, ReactWrapper, shallow } from "enzyme"; -import { Provider } from "react-redux"; -import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit"; +import {createBrowserHistory, createMemoryHistory, History, LocationState} from "history"; +import {EnzymePropSelector, mount, ReactWrapper, shallow} from "enzyme"; +import {Provider} from "react-redux"; +import {configureStore, getDefaultMiddleware} from "@reduxjs/toolkit"; import rootReducer from "../../src/app/store/reducers"; -import { ConnectedRouter, routerMiddleware } from "connected-react-router"; +import {ConnectedRouter, routerMiddleware} from "connected-react-router"; import { AxiosMockQueueItem, AxiosMockRequestCriteria, @@ -12,15 +12,15 @@ import { HttpResponse, } from "jest-mock-axios/dist/lib/mock-axios-types"; import mockAxios from "../__mocks__/axios"; -import { CONTEXT_PATH, SERVE_PATH } from "../../src/app/constants/path"; -import { mergeDeepRight } from "ramda"; -import { IStore } from "../../src/app/store/typings/IStore"; -import { render, waitFor } from "@testing-library/react"; +import {CONTEXT_PATH, SERVE_PATH} from "../../src/app/constants/path"; +import {mergeDeepRight} from "ramda"; +import {IStore} from "../../src/app/store/typings/IStore"; +import {render, RenderResult, waitFor} from "@testing-library/react"; import { responseInterceptorOnError, responseInterceptorOnSuccess, } from "../../src/app/services/fetch/responseInterceptor"; -import { AxiosError } from "axios"; +import {AxiosError} from "axios"; interface IMockValues { history: History; @@ -449,3 +449,56 @@ export const checkRequestMade = ( /** Cleans up the DOM. This is needed to avoid DOM elements from one test interfering with the subsequent tests. */ export const cleanUpDOM = () => (document.body.innerHTML = ""); + +export class RenderResultApi { + renderResult: RenderResult + + constructor(renderResult: RenderResult) { + this.renderResult = renderResult + } + + find = (cssSelector: string): Element | null => { + return this.renderResult.container.querySelector(cssSelector) + } + + findExisting = (cssSelector: string): Element => { + const element = this.find(cssSelector) + this.assert(!!element, `Element with selector '${cssSelector}' does not exist!`) + return element! + } + + findNth = (cssSelector: string, idx: number): Element => { + const element = this.findAll(cssSelector)[idx] + this.assert(!!element, `${idx + 1}th element with selector '${cssSelector}' does not exist!`) + return element! + } + + findAll = (cssSelector: string): NodeListOf => { + return this.renderResult.container.querySelectorAll(cssSelector) + } + + assert = (predicate: any, errorMessage: string) => { + if(!predicate) { + fail(errorMessage) + } + } + + click = (cssSelector: string, idx: number = 0) => { + const element = (this.findAll(cssSelector)[idx]) as HTMLButtonElement + this.assert(element, `No element with selector '${cssSelector}' ${idx !== 0 ? `at index ${idx} ` : ""}has been found!`) + if(element.click) { + element.click() + } else { + element.dispatchEvent(new Event('click')) + } + } + + printHtml = (selector?: string) => { + const elementToPrint = selector ? this.findExisting(selector) : this.renderResult.container + console.log(elementToPrint.outerHTML) + } + + static testId = (testId: string): string => { + return `[data-test-id = '${testId}']` + } +}