diff --git a/README.md b/README.md index 8e6067e8..275b1207 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ For the complete documentation, see the [docs site](https://rdfarchitect.soptim. - Import and export RDF schemas and CIM extensions - Visualize class structures via UML diagrams -- create custom diagrams containing classes from different packages or even profiles +- Create custom diagrams containing classes from different packages or even profiles +- Merge View — Visualize and inspect classes from multiple profiles across a dataset in a single unified diagram - Edit classes, attributes, associations, enum entries, and notes - Share diagrams as a complete, read-friendly reference of classes, relations, notes, and constraints - Schema migration between CIM versions and extensions diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramColorRestController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramColorRestController.java new file mode 100644 index 00000000..a1f4f413 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramColorRestController.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import io.swagger.v3.oas.annotations.Parameter; + +import lombok.RequiredArgsConstructor; + +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramColorDataDTO; +import org.rdfarchitect.services.diagrams.CrossProfileColorUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/datasets/{datasetName}/crossprofilediagramColors") +@RequiredArgsConstructor +public class CrossProfileDiagramColorRestController { + + private static final Logger logger = + LoggerFactory.getLogger(CrossProfileDiagramColorRestController.class); + + private final CrossProfileColorUseCase colorUseCase; + + @GetMapping + public CrossProfileDiagramColorDataDTO getCrossProfileColors( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName) { + logger.info( + "Received GET request: \"/api/datasets/{{}}/crossprofilediagramColors\" from \"{}\"", + datasetName, + originURL); + + var result = colorUseCase.getCrossProfileColors(datasetName); + + logger.info( + "Sending response to GET request: \"/api/datasets/{{}}/crossprofilediagramColors\" from \"{}\"", + datasetName, + originURL); + return result; + } + + @PutMapping + public void updateCrossProfileColors( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName, + @RequestBody CrossProfileDiagramColorDataDTO colorData) { + + logger.info( + "Received PUT request: \"/api/datasets/{{}}/crossprofilediagramColors\" from \"{}\"", + datasetName, + originURL); + + colorUseCase.replaceCrossProfileColors(datasetName, colorData); + + logger.info( + "Sending response to PUT request: \"/api/datasets/{{}}/crossprofilediagramColors\" from \"{}\"", + datasetName, + originURL); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramIDController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramIDController.java new file mode 100644 index 00000000..ed00cf15 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramIDController.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import io.swagger.v3.oas.annotations.Parameter; + +import lombok.RequiredArgsConstructor; + +import org.rdfarchitect.database.DatabasePort; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/datasets/{datasetName}/crossprofilediagramID") +@RequiredArgsConstructor +public class CrossProfileDiagramIDController { + + private static final Logger logger = + LoggerFactory.getLogger(CrossProfileDiagramIDController.class); + + private final DatabasePort databasePort; + + @GetMapping + public String getCrossProfileRenderingData( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName) { + logger.info( + "Received GET request: \"/api/datasets/{{}}/crossprofilediagram\" from \"{}\"", + datasetName, + originURL); + + logger.info( + "Sending response to GET request: \"/api/datasets/{{}}/crossprofilediagram\" from \"{}\"", + datasetName, + originURL); + return databasePort.getCrossProfileDiagramUUID(datasetName).toString(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRenderingRestController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRenderingRestController.java new file mode 100644 index 00000000..da92c706 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRenderingRestController.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import io.swagger.v3.oas.annotations.Parameter; + +import lombok.RequiredArgsConstructor; + +import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.models.cim.rendering.RenderCIMCollectionUseCase; +import org.rdfarchitect.services.diagrams.GetCustomDiagramsUseCase; +import org.rdfarchitect.services.rendering.DiagramToCIMCollectionConverterUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/datasets/{datasetName}/crossprofilediagramRendering") +@RequiredArgsConstructor +public class CrossProfileDiagramRenderingRestController { + + private static final Logger logger = + LoggerFactory.getLogger(CrossProfileDiagramRenderingRestController.class); + + private final DiagramToCIMCollectionConverterUseCase converter; + + private final RenderCIMCollectionUseCase renderer; + + private final GetCustomDiagramsUseCase getCustomDiagramsUseCase; + + private final DatabasePort databasePort; + + @GetMapping + public RenderingDataDTO getCrossProfileRenderingData( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName) { + logger.info( + "Received GET request: \"/api/datasets/{{}}/crossprofilediagramRendering\" from \"{}\"", + datasetName, + originURL); + + var crossProfileDiagram = + getCustomDiagramsUseCase.getCrossProfileDiagram(datasetName, true, true); + + var cimCollection = converter.convert(crossProfileDiagram); + + var result = + renderer.renderGlobalUML( + cimCollection, + datasetName, + databasePort.getCrossProfileDiagramUUID(datasetName)); + + logger.info( + "Sending response to GET request: \"/api/datasets/{{}}/crossprofilediagramRendering\" from \"{}\"", + datasetName, + originURL); + return result; + } +} diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRestController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRestController.java new file mode 100644 index 00000000..6ba71057 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRestController.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import io.swagger.v3.oas.annotations.Parameter; + +import lombok.RequiredArgsConstructor; + +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramDTO; +import org.rdfarchitect.services.diagrams.GetCustomDiagramsUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/datasets/{datasetName}/crossprofilediagram") +@RequiredArgsConstructor +public class CrossProfileDiagramRestController { + + private static final Logger logger = + LoggerFactory.getLogger(CrossProfileDiagramRestController.class); + + private final GetCustomDiagramsUseCase getCustomDiagramsUseCase; + + @GetMapping + public CrossProfileDiagramDTO getCrossProfileRenderingData( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName) { + logger.info( + "Received GET request: \"/api/datasets/{{}}/crossprofilediagram\" from \"{}\"", + datasetName, + originURL); + + var result = getCustomDiagramsUseCase.getCrossProfileDiagram(datasetName, false, false); + + logger.info( + "Sending response to GET request: \"/api/datasets/{{}}/crossprofilediagram\" from \"{}\"", + datasetName, + originURL); + return result; + } +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/ClassUMLAdaptedMapper.java b/backend/src/main/java/org/rdfarchitect/api/dto/ClassUMLAdaptedMapper.java index 0bcca5c6..8bb1e252 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/ClassUMLAdaptedMapper.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/ClassUMLAdaptedMapper.java @@ -54,6 +54,7 @@ public interface ClassUMLAdaptedMapper { List toDTOList(List cimClassList); @Mapping(target = "uri", source = ".") + @Mapping(target = "graphUri", ignore = true) CIMClassUMLAdapted toCIMObject(ClassUMLAdaptedDTO dto); List toCIMObjectList(List dtoList); diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/association/AssociationMapper.java b/backend/src/main/java/org/rdfarchitect/api/dto/association/AssociationMapper.java index 449cb1af..57971002 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/association/AssociationMapper.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/association/AssociationMapper.java @@ -48,6 +48,8 @@ public interface AssociationMapper { @Mapping(target = "uri", source = "dto") @Mapping(target = "domain", source = "dto") @Mapping(target = "inverseRoleName", expression = "java(buildInverseRoleName(inverseUri))") + @Mapping(target = "graphUri", ignore = true) + @Mapping(target = "color", ignore = true) CIMAssociation toCIMObject(AssociationDTO dto, String inverseUri); default String toDomain(URI value) { diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/attributes/AttributeMapper.java b/backend/src/main/java/org/rdfarchitect/api/dto/attributes/AttributeMapper.java index f1428f96..5bda17de 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/attributes/AttributeMapper.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/attributes/AttributeMapper.java @@ -67,6 +67,8 @@ default String map(URI value) { "java(new CIMSStereotype(\"http://iec.ch/TC57/NonStandard/UML#attribute\"))") @Mapping(target = "fixedValue", source = ".") @Mapping(target = "defaultValue", source = ".") + @Mapping(target = "graphUri", ignore = true) + @Mapping(target = "color", ignore = true) CIMAttribute toCIMObject(AttributeDTO dto); List toCIMObjectList(List dtoList); diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/ClassSourceDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/ClassSourceDTO.java new file mode 100644 index 00000000..1315ac7d --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/ClassSourceDTO.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.dto.crossProfileDiagram; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.UUID; + +@Data +@AllArgsConstructor +public class ClassSourceDTO { + private UUID classUUID; + private String graphUri; +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/CrossProfileDiagramColorDataDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/CrossProfileDiagramColorDataDTO.java new file mode 100644 index 00000000..b21130b3 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/CrossProfileDiagramColorDataDTO.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.dto.crossProfileDiagram; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Map; + +@Data +@AllArgsConstructor +public class CrossProfileDiagramColorDataDTO { + Map graphColors; +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/CrossProfileDiagramDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/CrossProfileDiagramDTO.java new file mode 100644 index 00000000..10c72fcd --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/CrossProfileDiagramDTO.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.dto.crossProfileDiagram; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class CrossProfileDiagramDTO { + private UUID diagramId; + private List classes; +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/GraphSourcedDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/GraphSourcedDTO.java new file mode 100644 index 00000000..0daf1317 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/GraphSourcedDTO.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.dto.crossProfileDiagram; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class GraphSourcedDTO { + private String graphUri; + private String graphColor; + private T value; +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/MergedClassDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/MergedClassDTO.java new file mode 100644 index 00000000..c39821ce --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/crossProfileDiagram/MergedClassDTO.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.dto.crossProfileDiagram; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import org.rdfarchitect.api.dto.SuperClassDTO; +import org.rdfarchitect.api.dto.association.AssociationPairDTO; +import org.rdfarchitect.api.dto.attributes.AttributeDTO; +import org.rdfarchitect.api.dto.enumentries.EnumEntryDTO; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; + +import java.util.List; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class MergedClassDTO { + + private UUID uuid; + private String classUri; + private String label; + private List sources; + private List> superClasses; + private List> attributes; + private List> enumEntries; + private List> associationPairs; + private List stereotypes; +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/enumentries/EnumEntryMapper.java b/backend/src/main/java/org/rdfarchitect/api/dto/enumentries/EnumEntryMapper.java index 88690781..ffa1eb14 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/enumentries/EnumEntryMapper.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/enumentries/EnumEntryMapper.java @@ -19,6 +19,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.mapstruct.factory.Mappers; import org.rdfarchitect.api.dto.MappingUtils; import org.rdfarchitect.models.cim.data.dto.CIMEnumEntry; @@ -40,13 +41,15 @@ public interface EnumEntryMapper { @Mapping(target = "label", source = "label.value") @Mapping(target = "prefix", source = "uri.prefix") @Mapping(target = "comment", source = "comment.value") - @Mapping(target = "type", source = "type.uri.suffix") + @Mapping(target = "type", source = "type", qualifiedByName = "mapTypeToString") EnumEntryDTO toDTO(CIMEnumEntry entity); List toDTOList(List entityList); @Mapping(target = "uri", source = ".") @Mapping(target = "type", source = ".") + @Mapping(target = "graphUri", ignore = true) + @Mapping(target = "color", ignore = true) CIMEnumEntry toCIMObject(EnumEntryDTO dto); List toCIMObjectList(List dtoList); @@ -67,4 +70,9 @@ default RDFType buildType(EnumEntryDTO dto) { default CIMSStereotype buildStereotype(String stereotype) { return stereotype != null ? new CIMSStereotype(CIMStereotypes.enumerationString) : null; } + + @Named("mapTypeToString") + default String mapTypeToString(RDFType type) { + return type != null ? type.getUri().toString() : null; + } } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/AttributeDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/AttributeDTO.java index a1366440..c5b9be64 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/AttributeDTO.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/AttributeDTO.java @@ -28,4 +28,6 @@ public class AttributeDTO { private String label; private String type; private String multiplicity; + private String graphUri; + private String color; } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/EdgeDataDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/EdgeDataDTO.java index 54a6f669..d07d4389 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/EdgeDataDTO.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/EdgeDataDTO.java @@ -29,4 +29,6 @@ public class EdgeDataDTO { private String fromMultiplicity; private boolean useToAssociation; private boolean useFromAssociation; + private String graphUri; + private String color; } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/EnumEntryDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/EnumEntryDTO.java new file mode 100644 index 00000000..8a41617e --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/EnumEntryDTO.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.dto.rendering.svelteflow.sub; + +import lombok.Builder; +import lombok.Data; + +/** DTO representing an enum entry used in the SvelteFlow node data DTO. */ +@Data +@Builder +public class EnumEntryDTO { + + private String label; + private String graphUri; + private String color; +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java index 664ca16d..349e89ea 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java @@ -32,5 +32,5 @@ public class NodeDataDTO { private String belongsToCategory; private List stereotypes; private List attributes; - private List enumEntries; + private List enumEntries; } diff --git a/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java b/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java index 6598db51..dedfe42e 100644 --- a/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java +++ b/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java @@ -52,6 +52,30 @@ public interface DatabasePort { */ DiagramLayout getDatasetDiagramLayout(String datasetName); + /** + * Returns the fixed UUID of the CrossProfileDiagram for the given dataset. + * + * @param datasetName literal dataset name + * @return UUID of the CrossProfileDiagram for the dataset + */ + UUID getCrossProfileDiagramUUID(String datasetName); + + /** + * Returns the color of the given graph in the CrossProfileDiagram. + * + * @param graphIdentifier The identifier of the graph. + * @return The color as hex code of the graph. + */ + String getCrossProfileDiagramColor(GraphIdentifier graphIdentifier); + + /** + * Sets the color of the given graph in the CrossProfileDiagram. + * + * @param graphIdentifier The identifier of the graph. + * @param color The color as hex code. + */ + void setCrossProfileDiagramColor(GraphIdentifier graphIdentifier, String color); + /** * Loads the namespace prefix mapping for the dataset. * diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java index d66abfd4..6f5c490e 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java @@ -62,6 +62,12 @@ public class GraphWithContextCollection { @Getter private final DiagramLayout diagramLayout = new DiagramLayout(); + @Getter private final UUID crossProfileDiagramUUID = UUID.randomUUID(); + + private final ConcurrentMap crossProfileDiagramColors = + new ConcurrentHashMap<>(); + + // universal prefix map for all graphs private final PrefixMapping prefixes = new PrefixMappingImpl(); @SuppressWarnings("java:S2093") @@ -231,4 +237,12 @@ private void assertValidGraphName(String graphUri) { throw new IllegalArgumentException("Graph Uri " + graphUri + " is not a valid URI"); } } + + public String getCrossProfileDiagramColor(String graphUri) { + return crossProfileDiagramColors.getOrDefault(graphUri, null); + } + + public void setCrossProfileDiagramColor(String graphUri, String color) { + crossProfileDiagramColors.put(graphUri, color); + } } diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java index 78c4ab59..dca0d4d4 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java @@ -72,6 +72,30 @@ public interface InMemoryDatabase { */ DiagramLayout getDatasetDiagramLayout(String datasetName); + /** + * Returns the fixed UUID of the CrossProfileDiagram for the given dataset. + * + * @param datasetName literal dataset name + * @return UUID of the CrossProfileDiagram for the dataset + */ + UUID getCrossProfileDiagramUUID(String datasetName); + + /** + * Returns the color of the given graph in the CrossProfileDiagram. + * + * @param graphIdentifier The identifier of the graph. + * @return The color as hex code of the graph. + */ + String getCrossProfileDiagramColor(GraphIdentifier graphIdentifier); + + /** + * Sets the color of the given graph in the CrossProfileDiagram. + * + * @param graphIdentifier The identifier of the graph. + * @param color The color as hex code. + */ + void setCrossProfileDiagramColor(GraphIdentifier graphIdentifier, String color); + /** * Creates a new named graph in a specified dataset. If the dataset does not exist yet, it will * be created. If the graph already exists, nothing happens. Merges the graph's prefix mapping diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java index 57d253f3..98281591 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java @@ -52,6 +52,21 @@ public DiagramLayout getDatasetDiagramLayout(String datasetName) { return database.getDatasetDiagramLayout(datasetName); } + @Override + public UUID getCrossProfileDiagramUUID(String datasetName) { + return database.getCrossProfileDiagramUUID(datasetName); + } + + @Override + public String getCrossProfileDiagramColor(GraphIdentifier graphIdentifier) { + return database.getCrossProfileDiagramColor(graphIdentifier); + } + + @Override + public void setCrossProfileDiagramColor(GraphIdentifier graphIdentifier, String color) { + database.setCrossProfileDiagramColor(graphIdentifier, color); + } + @Override public PrefixMapping getPrefixMapping(String datasetName) { return database.getPrefixMapping(datasetName); diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java index b24d4bd5..b1e247fc 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java @@ -31,6 +31,7 @@ import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; +import org.rdfarchitect.services.diagrams.CrossProfileUtils; import java.util.List; import java.util.Map; @@ -70,6 +71,21 @@ public DiagramLayout getDatasetDiagramLayout(String datasetName) { return getOrCreateSessionDataStore().getDatasetDiagramLayout(datasetName); } + @Override + public UUID getCrossProfileDiagramUUID(String datasetName) { + return getOrCreateSessionDataStore().getCrossProfileDiagramUUID(datasetName); + } + + @Override + public String getCrossProfileDiagramColor(GraphIdentifier graphIdentifier) { + return getOrCreateSessionDataStore().getCrossProfileDiagramColor(graphIdentifier); + } + + @Override + public void setCrossProfileDiagramColor(GraphIdentifier graphIdentifier, String color) { + getOrCreateSessionDataStore().setCrossProfileDiagramColor(graphIdentifier, color); + } + @Override public GraphContext getGraphWithContext(GraphIdentifier graphIdentifier) { return getOrCreateSessionDataStore().getGraphWithContext(graphIdentifier); @@ -84,6 +100,8 @@ public void createGraph(GraphIdentifier graphIdentifier, Graph newGraph) { .setNsPrefixes(store.getPrefixMapping(graphIdentifier.datasetName())) .setNsPrefixes(newGraph.getPrefixMapping()); store.setPrefixMapping(graphIdentifier.datasetName(), currentPrefixMapping); + store.setCrossProfileDiagramColor( + graphIdentifier, CrossProfileUtils.generateRandomDarkColor()); } @Override diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java index b4924489..0bebe50c 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java @@ -78,6 +78,30 @@ static Dataset wrapGraphInDataset(Graph graph, String graphUri) { */ DiagramLayout getDatasetDiagramLayout(String datasetName); + /** + * Returns the fixed UUID of the CrossProfileDiagram for the given dataset. + * + * @param datasetName literal dataset name + * @return UUID of the CrossProfileDiagram for the dataset + */ + UUID getCrossProfileDiagramUUID(String datasetName); + + /** + * Returns the color of the given graph in the CrossProfileDiagram. + * + * @param graphIdentifier The identifier of the graph. + * @return The color as hex code of the graph. + */ + String getCrossProfileDiagramColor(GraphIdentifier graphIdentifier); + + /** + * Sets the color of the given graph in the CrossProfileDiagram. + * + * @param graphIdentifier The identifier of the graph. + * @param color The color as hex code. + */ + void setCrossProfileDiagramColor(GraphIdentifier graphIdentifier, String color); + /** * Creates a new named graph in a specified dataset. If the dataset does not exist yet, it will * be created. If the graph already exists, nothing happens. diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java index 2cb9128d..c10a3055 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java @@ -120,6 +120,43 @@ public DiagramLayout getDatasetDiagramLayout(String datasetName) { } } + @Override + public UUID getCrossProfileDiagramUUID(String datasetName) { + lock.lock(); + try { + assertThatDatasetExists(datasetName); + return graphCollections.get(datasetName).getCrossProfileDiagramUUID(); + } finally { + lock.unlock(); + } + } + + @Override + public String getCrossProfileDiagramColor(GraphIdentifier graphIdentifier) { + lock.lock(); + try { + assertThatDatasetExists(graphIdentifier.datasetName()); + return graphCollections + .get(graphIdentifier.datasetName()) + .getCrossProfileDiagramColor(graphIdentifier.graphUri()); + } finally { + lock.unlock(); + } + } + + @Override + public void setCrossProfileDiagramColor(GraphIdentifier graphIdentifier, String color) { + lock.lock(); + try { + assertThatDatasetExists(graphIdentifier.datasetName()); + graphCollections + .get(graphIdentifier.datasetName()) + .setCrossProfileDiagramColor(graphIdentifier.graphUri(), color); + } finally { + lock.unlock(); + } + } + @Override public void create(GraphIdentifier graphIdentifier, Graph newGraph) { lock.lock(); diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/diagrams/GraphSourced.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/diagrams/GraphSourced.java new file mode 100644 index 00000000..b44da138 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/diagrams/GraphSourced.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.database.inmemory.diagrams; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class GraphSourced { + private List graphUris; + private T value; +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAssociation.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAssociation.java index c7382027..5835cd46 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAssociation.java @@ -39,6 +39,10 @@ public class CIMAssociation { private URI uri; + private String graphUri; + + private String color; + private RDFSLabel label; private RDFSComment comment; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAttribute.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAttribute.java index 673f322a..75709687 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAttribute.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMAttribute.java @@ -40,6 +40,10 @@ public class CIMAttribute { private URI uri; + private String graphUri; + + private String color; + private RDFSLabel label; private RDFSDomain domain; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMCollection.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMCollection.java index c4ed2f29..4c5776d3 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMCollection.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMCollection.java @@ -31,13 +31,22 @@ public CIMCollection() { classes = new TreeSet<>(Comparator.comparing(cimClass -> cimClass.getUri().toString())); attributes = new TreeSet<>( - Comparator.comparing(cimAttribute -> cimAttribute.getUri().toString())); + Comparator.comparing( + (CIMAttribute a) -> + a.getGraphUri() != null ? a.getGraphUri() : "") + .thenComparing(a -> a.getUri().toString())); associations = new TreeSet<>( - Comparator.comparing(cimAssociation -> cimAssociation.getUri().toString())); + Comparator.comparing((CIMAssociation a) -> a.getUri().toString()) + .thenComparing( + a -> a.getGraphUri() != null ? a.getGraphUri() : "")); enums = new TreeSet<>(Comparator.comparing(cimEnum -> cimEnum.getUri().toString())); enumEntries = - new TreeSet<>(Comparator.comparing(enumEntry -> enumEntry.getUri().toString())); + new TreeSet<>( + Comparator.comparing( + (CIMEnumEntry a) -> + a.getGraphUri() != null ? a.getGraphUri() : "") + .thenComparing(a -> a.getUri().toString())); } private final SortedSet packages; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMEnumEntry.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMEnumEntry.java index ec6d7e75..53d95d00 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMEnumEntry.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMEnumEntry.java @@ -36,6 +36,10 @@ public class CIMEnumEntry { private URI uri; + private String graphUri; + + private String color; + private RDFType type; private RDFSLabel label; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMMergedClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMMergedClass.java new file mode 100644 index 00000000..23b62a48 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/CIMMergedClass.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.models.cim.data.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import org.rdfarchitect.models.cim.data.dto.relations.RDFSSubClassOf; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder(toBuilder = true) +@NoArgsConstructor +public class CIMMergedClass extends CIMClass { + + private List superClasses; +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java index 2b10ca6d..1e2f9be9 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java @@ -23,12 +23,14 @@ import org.rdfarchitect.api.dto.rendering.svelteflow.sub.AttributeDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EdgeDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EdgeDataDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EnumEntryDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDataDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.PositionDTO; import org.rdfarchitect.models.cim.data.dto.CIMAssociation; import org.rdfarchitect.models.cim.data.dto.CIMClass; import org.rdfarchitect.models.cim.data.dto.CIMCollection; +import org.rdfarchitect.models.cim.data.dto.CIMMergedClass; import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; @@ -171,6 +173,8 @@ private List getClassAttributes(RenderContext renderContext, CIMCl .label(cimAttribute.getLabel().getValue()) .type(cimAttribute.getDataType().getLabel().getValue()) .multiplicity(extractMultiplicityString(cimAttribute.getMultiplicity())) + .graphUri(cimAttribute.getGraphUri()) + .color(cimAttribute.getColor()) .build()); } @@ -213,14 +217,19 @@ private List getClassStereotypes(CIMClass cimClass) { * @param cimClass The CIM class * @return List of enum entry labels */ - private List getClassEnumEntries(RenderContext renderContext, CIMClass cimClass) { - List enumEntries = new ArrayList<>(); + private List getClassEnumEntries(RenderContext renderContext, CIMClass cimClass) { + List enumEntries = new ArrayList<>(); for (var cimEnumEntry : renderContext.cimCollection.getEnumEntries()) { if (!cimEnumEntry.getType().getUri().equals(cimClass.getUri())) { continue; } - enumEntries.add(cimEnumEntry.getLabel().getValue()); + enumEntries.add( + EnumEntryDTO.builder() + .label(cimEnumEntry.getLabel().getValue()) + .graphUri(cimEnumEntry.getGraphUri()) + .color(cimEnumEntry.getColor()) + .build()); } return enumEntries; @@ -252,7 +261,18 @@ private List assembleInheritanceEdgeDTOList(RenderContext renderContext List inheritanceEdgeDTOList = new ArrayList<>(); for (var cimClass : renderContext.cimCollection.getClasses()) { if (cimClass.getSuperClass() != null) { - inheritanceEdgeDTOList.add(assembleInheritanceEdgeDTO(renderContext, cimClass)); + inheritanceEdgeDTOList.add( + assembleInheritanceEdgeDTO( + renderContext, + cimClass.getUri(), + cimClass.getSuperClass().getUri())); + } + if (cimClass instanceof CIMMergedClass cimMergedClass) { + for (var superClass : cimMergedClass.getSuperClasses()) { + inheritanceEdgeDTOList.add( + assembleInheritanceEdgeDTO( + renderContext, cimClass.getUri(), superClass.getUri())); + } } } return inheritanceEdgeDTOList; @@ -261,13 +281,14 @@ private List assembleInheritanceEdgeDTOList(RenderContext renderContext /** * Assembles an inheritance edge from a class to its superclass. * - * @param cimClass The child class + * @param classURI The URI of the child class + * @param superClassURI The URI of the parent class * @return EdgeDTO representing the inheritance relationship */ - private EdgeDTO assembleInheritanceEdgeDTO(RenderContext renderContext, CIMClass cimClass) { - var classUUID = renderContext.uriToUUIDMap.get(cimClass.getUri().toString()); - var superClassUUID = - renderContext.uriToUUIDMap.get(cimClass.getSuperClass().getUri().toString()); + private EdgeDTO assembleInheritanceEdgeDTO( + RenderContext renderContext, URI classURI, URI superClassURI) { + var classUUID = renderContext.uriToUUIDMap.get(classURI.toString()); + var superClassUUID = renderContext.uriToUUIDMap.get(superClassURI.toString()); return EdgeDTO.builder() .id(UUID.randomUUID().toString()) @@ -334,6 +355,8 @@ private EdgeDTO assembleAssociationEdgeDTO( .fromMultiplicity(fromMultiplicity) .useToAssociation(useToAssociation) .useFromAssociation(useFromAssociation) + .graphUri(from.getGraphUri()) + .color(from.getColor()) .build(); return EdgeDTO.builder() diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/DiagramLayout.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/DiagramLayout.java index 36737408..c3d47b9c 100644 --- a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/DiagramLayout.java +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/DiagramLayout.java @@ -30,14 +30,11 @@ public class DiagramLayout { @Getter private final Model diagramLayoutModel; - @Getter private final MRID defaultPackageMRID; public DiagramLayout() { defaultPackageMRID = new MRID(UUID.randomUUID()); - diagramLayoutModel = ModelFactory.createDefaultModel(); - diagramLayoutModel.setNsPrefix(CIM.PREFIX, CIM.NAMESPACE); diagramLayoutModel.setNsPrefix("rdf", RDF.uri); } diff --git a/backend/src/main/java/org/rdfarchitect/services/diagrams/CrossProfileColorUseCase.java b/backend/src/main/java/org/rdfarchitect/services/diagrams/CrossProfileColorUseCase.java new file mode 100644 index 00000000..d22a9ba2 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/diagrams/CrossProfileColorUseCase.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.services.diagrams; + +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramColorDataDTO; + +public interface CrossProfileColorUseCase { + + /** + * Returns the colors of the graphs used in the cross profile diagram. + * + * @param datasetName The dataset of the diagram. + * @return The colors of the graphs. + */ + CrossProfileDiagramColorDataDTO getCrossProfileColors(String datasetName); + + /** + * Replaces the colors of the graphs used in the cross profile diagram. + * + * @param datasetName The dataset of the diagram. + */ + void replaceCrossProfileColors(String datasetName, CrossProfileDiagramColorDataDTO dto); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/diagrams/CrossProfileUtils.java b/backend/src/main/java/org/rdfarchitect/services/diagrams/CrossProfileUtils.java new file mode 100644 index 00000000..d85a5f04 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/diagrams/CrossProfileUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.services.diagrams; + +import lombok.experimental.UtilityClass; + +import java.awt.Color; +import java.util.Random; + +@UtilityClass +public class CrossProfileUtils { + + private static final Random random = new Random(); + + public static String generateRandomDarkColor() { + float hue = random.nextFloat(); + float saturation = 0.5f + random.nextFloat() * 0.5f; + float brightness = 0.3f + random.nextFloat() * 0.4f; + + Color color = Color.getHSBColor(hue, saturation, brightness); + return String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue()); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java b/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java index 075c6836..f5f05645 100644 --- a/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java +++ b/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java @@ -20,14 +20,33 @@ import lombok.RequiredArgsConstructor; import org.apache.jena.query.ReadWrite; +import org.rdfarchitect.api.dto.ClassUMLAdaptedDTO; +import org.rdfarchitect.api.dto.crossProfileDiagram.ClassSourceDTO; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramColorDataDTO; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramDTO; +import org.rdfarchitect.api.dto.crossProfileDiagram.GraphSourcedDTO; +import org.rdfarchitect.api.dto.crossProfileDiagram.MergedClassDTO; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; +import org.rdfarchitect.dl.data.dto.DiagramObject; +import org.rdfarchitect.dl.data.dto.relations.MRID; +import org.rdfarchitect.dl.queries.select.DLObjectFetcher; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; +import org.rdfarchitect.services.dl.update.DiagramLayoutServiceUtils; +import org.rdfarchitect.services.select.GetClassListUseCase; import org.springframework.stereotype.Service; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -35,9 +54,11 @@ public class CustomDiagramService implements GetCustomDiagramsUseCase, ReplaceCustomDiagramUseCase, DeleteCustomDiagramUseCase, - RemoveFromDiagramUseCase { + RemoveFromDiagramUseCase, + CrossProfileColorUseCase { private final DatabasePort databasePort; + private final GetClassListUseCase getClassListUseCase; @Override public List getCustomDiagramsForGraph(GraphIdentifier graphIdentifier) { @@ -51,6 +72,146 @@ public List getCustomDiagramsForDataset(String datasetName) { return databasePort.getDatasetDiagrams(datasetName).values().stream().toList(); } + @Override + public CrossProfileDiagramDTO getCrossProfileDiagram( + String datasetName, boolean includeProperties, boolean doLayout) { + var graphUris = databasePort.listGraphUris(datasetName); + var crossProfileDiagramUUID = databasePort.getCrossProfileDiagramUUID(datasetName); + var diagramLayout = databasePort.getDatasetDiagramLayout(datasetName); + + Map mergeMap = new LinkedHashMap<>(); + + for (var graphUri : graphUris) { + var graphIdentifier = new GraphIdentifier(datasetName, graphUri); + List classList; + if (includeProperties) { + classList = getClassListUseCase.getFullClassList(graphIdentifier); + } else { + classList = getClassListUseCase.getClassList(graphIdentifier, false); + } + var graphColor = databasePort.getCrossProfileDiagramColor(graphIdentifier); + + for (var dto : classList) { + var classUri = dto.getPrefix() + dto.getLabel(); + var mergedUuid = UUID.nameUUIDFromBytes(classUri.getBytes(StandardCharsets.UTF_8)); + + var merged = + mergeMap.computeIfAbsent( + classUri, + uri -> + new MergedClassDTO( + mergedUuid, + uri, + dto.getLabel(), + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>())); + + merged.getSources().add(new ClassSourceDTO(dto.getUuid(), graphUri)); + + if (includeProperties) { + mergeProperties(graphUri, dto, merged, graphColor); + } + } + } + if (doLayout) { + doDiagramLayout(diagramLayout, crossProfileDiagramUUID, mergeMap); + } + return new CrossProfileDiagramDTO( + crossProfileDiagramUUID, new ArrayList<>(mergeMap.values())); + } + + private static void mergeProperties( + String graphUri, ClassUMLAdaptedDTO dto, MergedClassDTO merged, String graphColor) { + if (dto.getAttributes() != null) { + mergeAttributes(graphUri, dto, merged, graphColor); + } + if (dto.getEnumEntries() != null) { + mergeEnumEntries(graphUri, dto, merged, graphColor); + } + if (dto.getAssociationPairs() != null) { + mergeAssociationPairs(graphUri, dto, merged, graphColor); + } + if (dto.getSuperClass() != null) { + merged.getSuperClasses() + .add(new GraphSourcedDTO<>(graphUri, graphColor, dto.getSuperClass())); + } + if (dto.getStereotypes() != null) { + mergeStereotypes(dto, merged); + } + } + + private static void mergeAttributes( + String graphUri, ClassUMLAdaptedDTO dto, MergedClassDTO merged, String graphColor) { + dto.getAttributes() + .forEach( + attr -> + merged.getAttributes() + .add(new GraphSourcedDTO<>(graphUri, graphColor, attr))); + } + + private static void mergeEnumEntries( + String graphUri, ClassUMLAdaptedDTO dto, MergedClassDTO merged, String graphColor) { + dto.getEnumEntries() + .forEach( + entry -> + merged.getEnumEntries() + .add(new GraphSourcedDTO<>(graphUri, graphColor, entry))); + } + + private static void mergeAssociationPairs( + String graphUri, ClassUMLAdaptedDTO dto, MergedClassDTO merged, String graphColor) { + dto.getAssociationPairs() + .forEach( + assoc -> + merged.getAssociationPairs() + .add(new GraphSourcedDTO<>(graphUri, graphColor, assoc))); + } + + private static void mergeStereotypes(ClassUMLAdaptedDTO dto, MergedClassDTO merged) { + dto.getStereotypes() + .forEach( + stereotype -> { + if (!merged.getStereotypes().contains(new CIMSStereotype(stereotype))) { + merged.getStereotypes().add(new CIMSStereotype(stereotype)); + } + }); + } + + private static void doDiagramLayout( + DiagramLayout diagramLayout, + UUID crossProfileDiagramUUID, + Map mergeMap) { + + var model = diagramLayout.getDiagramLayoutModel(); + if (DLObjectFetcher.fetchDiagram(model, crossProfileDiagramUUID) == null) { + DiagramLayoutServiceUtils.insertDiagram( + model, crossProfileDiagramUUID, "CrossProfileDiagram"); + } + var existingDOs = DLObjectFetcher.fetchDiagramDOs(model, new MRID(crossProfileDiagramUUID)); + var existingClassUUIDs = + existingDOs.stream() + .map(DiagramObject::getBelongsToIdentifiedObject) + .map(MRID::getUuid) + .collect(Collectors.toSet()); + + for (var merged : mergeMap.values()) { + if (!existingClassUUIDs.contains(merged.getUuid())) { + var doMRID = + DiagramLayoutServiceUtils.insertDiagramObject( + model, + crossProfileDiagramUUID, + merged.getClassUri(), + merged.getUuid()); + DiagramLayoutServiceUtils.insertDiagramObjectPoint( + model, crossProfileDiagramUUID, doMRID); + } + } + } + @Override public void deleteCustomDiagram(String datasetName, String diagramId) { var diagrams = databasePort.getDatasetDiagrams(datasetName); @@ -137,4 +298,28 @@ public void removeFromAllDiagrams(GraphIdentifier graphIdentifier, UUID classId) ctx.commit("removed class %s from all diagrams".formatted(classId)); } } + + @Override + public CrossProfileDiagramColorDataDTO getCrossProfileColors(String datasetName) { + var graphUris = databasePort.listGraphUris(datasetName); + var colorsDTO = new CrossProfileDiagramColorDataDTO(new HashMap<>()); + for (var graphUri : graphUris) { + var graphIdentifier = new GraphIdentifier(datasetName, graphUri); + var graphColor = databasePort.getCrossProfileDiagramColor(graphIdentifier); + colorsDTO.getGraphColors().put(graphUri, graphColor); + } + return colorsDTO; + } + + @Override + public void replaceCrossProfileColors(String datasetName, CrossProfileDiagramColorDataDTO dto) { + var graphUris = databasePort.listGraphUris(datasetName); + for (var graphUri : graphUris) { + var graphIdentifier = new GraphIdentifier(datasetName, graphUri); + if (dto.getGraphColors().containsKey(graphUri)) { + databasePort.setCrossProfileDiagramColor( + graphIdentifier, dto.getGraphColors().get(graphUri)); + } + } + } } diff --git a/backend/src/main/java/org/rdfarchitect/services/diagrams/GetCustomDiagramsUseCase.java b/backend/src/main/java/org/rdfarchitect/services/diagrams/GetCustomDiagramsUseCase.java index 21c9f02b..b53bf38c 100644 --- a/backend/src/main/java/org/rdfarchitect/services/diagrams/GetCustomDiagramsUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/diagrams/GetCustomDiagramsUseCase.java @@ -17,6 +17,7 @@ package org.rdfarchitect.services.diagrams; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramDTO; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; @@ -39,4 +40,13 @@ public interface GetCustomDiagramsUseCase { * @return The custom diagrams for the dataset. */ List getCustomDiagramsForDataset(String datasetName); + + /** + * Returns the cross profile diagram for the dataset. + * + * @param datasetName The name of the dataset. + * @return The cross profile diagram for the dataset. + */ + CrossProfileDiagramDTO getCrossProfileDiagram( + String datasetName, boolean includeProperties, boolean doLayout); } diff --git a/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java b/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java index edbe383e..6a0e3549 100644 --- a/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java +++ b/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java @@ -42,10 +42,10 @@ public class QueryDiagramLayoutService implements FetchRenderingLayoutDataUseCas public RenderingLayoutData fetchRenderingLayoutData( GraphIdentifier graphIdentifier, UUID packageUUID) { try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { - DiagramLayoutDelta diagramLayout = ctx.getDiagramLayout(); - var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); + DiagramLayoutDelta diagramLayoutDelta = ctx.getDiagramLayout(); + var diagramLayoutModel = diagramLayoutDelta.getDiagramLayoutModel(); return fetchRenderingLayoutData( - diagramLayout.getDefaultPackageMRID().getUuid(), + diagramLayoutDelta.getDefaultPackageMRID().getUuid(), diagramLayoutModel, packageUUID); } diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java b/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java index b196f1e5..c2aec234 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java @@ -19,13 +19,24 @@ import lombok.RequiredArgsConstructor; +import org.rdfarchitect.api.dto.association.AssociationPairMapper; +import org.rdfarchitect.api.dto.attributes.AttributeMapper; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramDTO; +import org.rdfarchitect.api.dto.crossProfileDiagram.MergedClassDTO; +import org.rdfarchitect.api.dto.enumentries.EnumEntryMapper; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import org.rdfarchitect.models.cim.data.dto.CIMCollection; +import org.rdfarchitect.models.cim.data.dto.CIMMergedClass; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSSubClassOf; +import org.rdfarchitect.models.cim.data.dto.relations.RDFType; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rendering.GraphFilter; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.UUID; import java.util.stream.Collectors; @@ -38,6 +49,10 @@ public class DiagramToCIMCollectionConverterService private final GraphToCIMCollectionConverterService converter; + private final AttributeMapper attributeMapper; + private final EnumEntryMapper enumEntryMapper; + private final AssociationPairMapper associationPairMapper; + @Override public CIMCollection convert(GraphIdentifier graphIdentifier, String diagramId) { var diagrams = databasePort.getGraphWithContext(graphIdentifier).getCustomDiagrams(); @@ -96,4 +111,83 @@ private void mergeInto(CIMCollection target, CIMCollection source) { target.getEnumEntries().addAll(source.getEnumEntries()); target.getAssociations().addAll(source.getAssociations()); } + + @Override + public CIMCollection convert(CrossProfileDiagramDTO diagram) { + var collection = new CIMCollection(); + + for (var mergedClass : diagram.getClasses()) { + var uri = new URI(mergedClass.getClassUri()); + var superClasses = new ArrayList(); + for (var superClassDTO : mergedClass.getSuperClasses()) { + var superClass = superClassDTO.getValue(); + superClasses.add( + new RDFSSubClassOf( + new URI(superClass.getPrefix() + superClass.getLabel()), + new RDFSLabel(superClass.getLabel()))); + } + var label = new RDFSLabel(uri.getSuffix()); + var cimClass = + CIMMergedClass.builder() + .uuid(mergedClass.getUuid()) + .uri(uri) + .label(label) + .superClasses(superClasses) + .stereotypes(mergedClass.getStereotypes()) + .build(); + collection.getClasses().add(cimClass); + + convertAttributes(mergedClass, collection); + convertEnumEntries(mergedClass, collection); + convertAssociationPairs(mergedClass, collection); + } + + return collection; + } + + private void convertAssociationPairs(MergedClassDTO mergedClass, CIMCollection collection) { + for (var graphSourcedPair : mergedClass.getAssociationPairs()) { + var pair = associationPairMapper.toCIMObject(graphSourcedPair.getValue()); + var graphUri = graphSourcedPair.getGraphUri(); + var color = graphSourcedPair.getGraphColor(); + if (pair.getFrom() != null) { + collection + .getAssociations() + .add(pair.getFrom().toBuilder().graphUri(graphUri).color(color).build()); + } + if (pair.getTo() != null) { + collection + .getAssociations() + .add(pair.getTo().toBuilder().graphUri(graphUri).color(color).build()); + } + } + } + + private void convertEnumEntries(MergedClassDTO mergedClass, CIMCollection collection) { + for (var graphSourcedEntry : mergedClass.getEnumEntries()) { + var cimEnumEntry = enumEntryMapper.toCIMObject(graphSourcedEntry.getValue()); + cimEnumEntry = + cimEnumEntry.toBuilder() + .type( + new RDFType( + new URI(mergedClass.getClassUri()), + new RDFSLabel(graphSourcedEntry.getValue().getLabel()))) + .graphUri(graphSourcedEntry.getGraphUri()) + .color(graphSourcedEntry.getGraphColor()) + .build(); + collection.getEnumEntries().add(cimEnumEntry); + } + } + + private void convertAttributes(MergedClassDTO mergedClass, CIMCollection collection) { + for (var graphSourcedAttr : mergedClass.getAttributes()) { + var cimAttribute = attributeMapper.toCIMObject(graphSourcedAttr.getValue()); + cimAttribute = + cimAttribute.toBuilder() + .graphUri(graphSourcedAttr.getGraphUri()) + .color(graphSourcedAttr.getGraphColor()) + .build(); + collection.getAttributes().add(cimAttribute); + } + } } diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterUseCase.java b/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterUseCase.java index 8302a191..b2218219 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterUseCase.java @@ -17,6 +17,7 @@ package org.rdfarchitect.services.rendering; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramDTO; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.data.dto.CIMCollection; @@ -40,4 +41,12 @@ public interface DiagramToCIMCollectionConverterUseCase { * @return The {@link CIMCollection}. */ CIMCollection convert(String datasetName, String diagramId); + + /** + * Converts a Diagram to a {@link CIMCollection}. + * + * @param diagram The diagram to be converted. + * @return The {@link CIMCollection}. + */ + CIMCollection convert(CrossProfileDiagramDTO diagram); } diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java b/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java index 5baff55f..2d350a27 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java @@ -372,7 +372,12 @@ private void fetchAttributes( databasePort.getPrefixMapping(graphIdentifier.datasetName())) .fetchCIMAttributeList(attributesQuery.build()); - attributeList.forEach(cimAttribute -> cimCollection.getAttributes().add(cimAttribute)); + attributeList.forEach( + cimAttribute -> { + cimAttribute = + cimAttribute.toBuilder().graphUri(graphIdentifier.graphUri()).build(); + cimCollection.getAttributes().add(cimAttribute); + }); } // enums @@ -460,7 +465,12 @@ private void fetchEnumEntries( databasePort.getPrefixMapping(graphIdentifier.datasetName())) .fetchCIMEnumEntryList(enumEntriesQuery.build()); - enumEntryList.forEach(cimEnumEntry -> cimCollection.getEnumEntries().add(cimEnumEntry)); + enumEntryList.forEach( + cimEnumEntry -> { + cimEnumEntry = + cimEnumEntry.toBuilder().graphUri(graphIdentifier.graphUri()).build(); + cimCollection.getEnumEntries().add(cimEnumEntry); + }); } private void fetchAssociations( @@ -499,11 +509,19 @@ private void fetchAssociations( associationList.forEach( cimAssociation -> { + var finalCimAssociation = cimAssociation; if (cimCollection.getClasses().stream() .anyMatch( cimClass -> cimClass.getUri() - .equals(cimAssociation.getRange().getUri()))) { + .equals( + finalCimAssociation + .getRange() + .getUri()))) { + cimAssociation = + cimAssociation.toBuilder() + .graphUri(graphIdentifier.graphUri()) + .build(); cimCollection.getAssociations().add(cimAssociation); } }); diff --git a/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java b/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java index ebb6d421..f8e85418 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java @@ -33,4 +33,12 @@ public interface GetClassListUseCase { */ List getClassList( GraphIdentifier graphIdentifier, boolean includeExternalClasses); + + /** + * Gets the list of classes in the graph with full information. + * + * @param graphIdentifier The graph to getClassDefinition. + * @return The list of classes in the graph. + */ + List getFullClassList(GraphIdentifier graphIdentifier); } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java index 51616e7a..5e82a1e1 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java @@ -186,6 +186,29 @@ private List getReferencedClassList(GraphIdentifier graphIde return CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); } + @Override + public List getFullClassList(GraphIdentifier graphIdentifier) { + var prefixMapping = databasePort.getPrefixMapping(graphIdentifier.datasetName()); + var baseList = getClassList(graphIdentifier, false); + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return baseList.stream() + .filter(dto -> dto.getUuid() != null) + .map( + dto -> { + var fullClass = + CIMUMLObjectFactory.createCIMClassUMLAdapted( + ctx.getRdfGraph(), + graphIdentifier.graphUri(), + prefixMapping, + dto.getUuid().toString()); + fullClass.nullEmptyLists(); + return classMapper.toDTO(fullClass); + }) + .toList(); + } + } + @Override public List listDatatypes(GraphIdentifier graphIdentifier) { // build query diff --git a/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramColorRestControllerTest.java b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramColorRestControllerTest.java new file mode 100644 index 00000000..395ca156 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramColorRestControllerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramColorDataDTO; +import org.rdfarchitect.services.diagrams.CrossProfileColorUseCase; +import org.springframework.http.HttpHeaders; + +import java.util.Map; + +class CrossProfileDiagramColorRestControllerTest { + + private CrossProfileColorUseCase colorUseCase; + private CrossProfileDiagramColorRestController controller; + + @BeforeEach + void setUp() { + colorUseCase = mock(CrossProfileColorUseCase.class); + controller = new CrossProfileDiagramColorRestController(colorUseCase); + } + + @Test + void getCrossProfileColors_returnsDTOFromUseCase() { + var expectedDTO = new CrossProfileDiagramColorDataDTO(Map.of("graph-a", "#ff0000")); + when(colorUseCase.getCrossProfileColors("my-dataset")).thenReturn(expectedDTO); + + var result = controller.getCrossProfileColors(HttpHeaders.ORIGIN, "my-dataset"); + + assertThat(result).isEqualTo(expectedDTO); + verify(colorUseCase).getCrossProfileColors("my-dataset"); + } + + @Test + void updateCrossProfileColors_invokesUseCaseWithPayload() { + var colorData = new CrossProfileDiagramColorDataDTO(Map.of("graph-a", "#00ff00")); + + controller.updateCrossProfileColors(HttpHeaders.ORIGIN, "my-dataset", colorData); + + verify(colorUseCase).replaceCrossProfileColors("my-dataset", colorData); + } +} diff --git a/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramIDControllerTest.java b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramIDControllerTest.java new file mode 100644 index 00000000..4f4574d5 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramIDControllerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.rdfarchitect.database.DatabasePort; +import org.springframework.http.HttpHeaders; + +import java.util.UUID; + +class CrossProfileDiagramIDControllerTest { + + private DatabasePort databasePort; + private CrossProfileDiagramIDController controller; + + @BeforeEach + void setUp() { + databasePort = mock(DatabasePort.class); + controller = new CrossProfileDiagramIDController(databasePort); + } + + @Test + void getCrossProfileData_returnsUUIDStringFromDatabasePort() { + var uuid = UUID.randomUUID(); + when(databasePort.getCrossProfileDiagramUUID("my-dataset")).thenReturn(uuid); + + var result = controller.getCrossProfileRenderingData(HttpHeaders.ORIGIN, "my-dataset"); + + assertThat(result).isEqualTo(uuid.toString()); + verify(databasePort).getCrossProfileDiagramUUID("my-dataset"); + } +} diff --git a/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRenderingRestControllerTest.java b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRenderingRestControllerTest.java new file mode 100644 index 00000000..0df1e895 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRenderingRestControllerTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramDTO; +import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.models.cim.data.dto.CIMCollection; +import org.rdfarchitect.models.cim.rendering.RenderCIMCollectionUseCase; +import org.rdfarchitect.services.diagrams.GetCustomDiagramsUseCase; +import org.rdfarchitect.services.rendering.DiagramToCIMCollectionConverterUseCase; +import org.springframework.http.HttpHeaders; + +import java.util.List; +import java.util.UUID; + +class CrossProfileDiagramRenderingRestControllerTest { + + private DiagramToCIMCollectionConverterUseCase converter; + private RenderCIMCollectionUseCase renderer; + private GetCustomDiagramsUseCase getCustomDiagramsUseCase; + private DatabasePort databasePort; + private CrossProfileDiagramRenderingRestController controller; + + @BeforeEach + void setUp() { + converter = mock(DiagramToCIMCollectionConverterUseCase.class); + renderer = mock(RenderCIMCollectionUseCase.class); + getCustomDiagramsUseCase = mock(GetCustomDiagramsUseCase.class); + databasePort = mock(DatabasePort.class); + controller = + new CrossProfileDiagramRenderingRestController( + converter, renderer, getCustomDiagramsUseCase, databasePort); + } + + @Test + void getCrossProfileRenderingData_orchestratesUseCasesAndReturnsRenderingDTO() { + var diagramUUID = UUID.randomUUID(); + var diagram = new CrossProfileDiagramDTO(diagramUUID, List.of()); + var cimCollection = mock(CIMCollection.class); + var expectedRendering = mock(RenderingDataDTO.class); + + when(getCustomDiagramsUseCase.getCrossProfileDiagram("my-dataset", true, true)) + .thenReturn(diagram); + when(converter.convert(diagram)).thenReturn(cimCollection); + when(databasePort.getCrossProfileDiagramUUID("my-dataset")).thenReturn(diagramUUID); + when(renderer.renderGlobalUML(cimCollection, "my-dataset", diagramUUID)) + .thenReturn(expectedRendering); + + var result = controller.getCrossProfileRenderingData(HttpHeaders.ORIGIN, "my-dataset"); + + assertThat(result).isEqualTo(expectedRendering); + verify(getCustomDiagramsUseCase).getCrossProfileDiagram("my-dataset", true, true); + verify(converter).convert(diagram); + verify(databasePort).getCrossProfileDiagramUUID("my-dataset"); + verify(renderer).renderGlobalUML(cimCollection, "my-dataset", diagramUUID); + } +} diff --git a/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRestControllerTest.java b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRestControllerTest.java new file mode 100644 index 00000000..0bcd61ed --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/api/controller/datasets/diagrams/CrossProfileDiagramRestControllerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.api.controller.datasets.diagrams; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.rdfarchitect.api.dto.crossProfileDiagram.CrossProfileDiagramDTO; +import org.rdfarchitect.services.diagrams.GetCustomDiagramsUseCase; +import org.springframework.http.HttpHeaders; + +import java.util.List; +import java.util.UUID; + +class CrossProfileDiagramRestControllerTest { + + private GetCustomDiagramsUseCase getCustomDiagramsUseCase; + private CrossProfileDiagramRestController controller; + + @BeforeEach + void setUp() { + getCustomDiagramsUseCase = mock(GetCustomDiagramsUseCase.class); + controller = new CrossProfileDiagramRestController(getCustomDiagramsUseCase); + } + + @Test + void getCrossProfileData_returnsDTOFromUseCase() { + var expectedDTO = new CrossProfileDiagramDTO(UUID.randomUUID(), List.of()); + when(getCustomDiagramsUseCase.getCrossProfileDiagram("my-dataset", false, false)) + .thenReturn(expectedDTO); + + var result = controller.getCrossProfileRenderingData(HttpHeaders.ORIGIN, "my-dataset"); + + assertThat(result).isEqualTo(expectedDTO); + verify(getCustomDiagramsUseCase).getCrossProfileDiagram("my-dataset", false, false); + } +} diff --git a/backend/src/test/java/org/rdfarchitect/api/dto/EnumEntryMapperTest.java b/backend/src/test/java/org/rdfarchitect/api/dto/EnumEntryMapperTest.java index 4c61072b..acba30cb 100644 --- a/backend/src/test/java/org/rdfarchitect/api/dto/EnumEntryMapperTest.java +++ b/backend/src/test/java/org/rdfarchitect/api/dto/EnumEntryMapperTest.java @@ -75,7 +75,7 @@ void toDto_minimalEnumEntry() { () -> assertThat(dto.getUuid()).isEqualTo(cimEnumEntry.getUuid()), () -> assertThat(dto.getPrefix()).isEqualTo("http://example.org#"), () -> assertThat(dto.getLabel()).isEqualTo("TestEnumEntry"), - () -> assertThat(dto.getType()).isEqualTo("TestEnum"), + () -> assertThat(dto.getType()).isEqualTo("http://example.org#TestEnum"), () -> assertThat(dto.getComment()).isNull(), () -> assertThat(dto.getStereotype()).isNull()); } @@ -94,7 +94,7 @@ void toDTO_fullEnumEntry() { () -> assertThat(dto.getUuid()).isEqualTo(cimEnumEntry.getUuid()), () -> assertThat(dto.getPrefix()).isEqualTo("http://example.org#"), () -> assertThat(dto.getLabel()).isEqualTo("TestEnumEntry"), - () -> assertThat(dto.getType()).isEqualTo("TestEnum"), + () -> assertThat(dto.getType()).isEqualTo("http://example.org#TestEnum"), () -> assertThat(dto.getComment()).isEqualTo("Test comment"), () -> assertThat(dto.getStereotype()).isEqualTo("enum")); } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java index 4f929ea4..a8fb0465 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java @@ -23,6 +23,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.rdfarchitect.api.dto.association.AssociationPairMapper; +import org.rdfarchitect.api.dto.attributes.AttributeMapper; +import org.rdfarchitect.api.dto.enumentries.EnumEntryMapper; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; @@ -45,6 +48,9 @@ class DiagramToCIMCollectionConverterServiceTest { private DatabasePort databasePort; private GraphToCIMCollectionConverterService converter; + private AttributeMapper attributeMapper; + private EnumEntryMapper enumEntryMapper; + private AssociationPairMapper associationPairMapper; private DiagramToCIMCollectionConverterService service; private GraphIdentifier graphIdentifier; @@ -53,7 +59,16 @@ class DiagramToCIMCollectionConverterServiceTest { void setUp() { databasePort = mock(DatabasePort.class); converter = mock(GraphToCIMCollectionConverterService.class); - service = new DiagramToCIMCollectionConverterService(databasePort, converter); + attributeMapper = mock(AttributeMapper.class); + enumEntryMapper = mock(EnumEntryMapper.class); + associationPairMapper = mock(AssociationPairMapper.class); + service = + new DiagramToCIMCollectionConverterService( + databasePort, + converter, + attributeMapper, + enumEntryMapper, + associationPairMapper); graphIdentifier = new GraphIdentifier("dataset", "http://example.org#graph"); } diff --git a/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java index 27be56ce..8c5eff86 100644 --- a/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java @@ -30,6 +30,7 @@ import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.services.select.GetClassListUseCase; import java.util.ArrayList; import java.util.List; @@ -46,7 +47,8 @@ class CustomDiagramsServiceTest { @BeforeEach void setUp() { databasePort = mock(DatabasePort.class); - service = new CustomDiagramService(databasePort); + var getClassListUseCase = mock(GetClassListUseCase.class); + service = new CustomDiagramService(databasePort, getClassListUseCase); graphIdentifier = mock(GraphIdentifier.class); when(graphIdentifier.graphUri()).thenReturn("http://example.org#graph"); diff --git a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java index b443383e..b3a1a218 100644 --- a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java @@ -22,6 +22,7 @@ import org.apache.jena.datatypes.xsd.XSDDatatype; import org.junit.jupiter.api.Test; import org.rdfarchitect.api.dto.rendering.svelteflow.SvelteFlowDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EnumEntryDTO; import org.rdfarchitect.models.cim.data.dto.relations.CIMSBelongsToCategory; import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; @@ -136,8 +137,8 @@ void renderUML_singleEnum_createsNodeWithCorrectData() { .contains("abstract"); assertThat(nodeDTO.getData().getEnumEntries()) .hasSize(2) - .contains("enumEntry1") - .contains("enumEntry2"); + .extracting(EnumEntryDTO::getLabel) + .contains("enumEntry1", "enumEntry2"); } @Test diff --git a/frontend/src/lib/api/apiDatasetUtils.js b/frontend/src/lib/api/apiDatasetUtils.js index 847ca615..e2a1aaf9 100644 --- a/frontend/src/lib/api/apiDatasetUtils.js +++ b/frontend/src/lib/api/apiDatasetUtils.js @@ -48,3 +48,9 @@ export async function getDatasetNames() { } return { modifiable: modifiableDatasets, readonly: readOnlyDatasets }; } + +export async function getCrossProfileDiagram(datasetName) { + if (!datasetName) return null; + const res = await bec.getCrossProfileDiagramForDataset(datasetName); + return await res.json(); +} diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index 470bdf78..cad7e170 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -46,6 +46,16 @@ export class BackendConnection { }); } + async getCrossProfileID(datasetName) { + const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/crossprofilediagramID`; + return fetch(url, { + method: "GET", + mode: "cors", + headers: new Headers({ "Content-Type": "application/json" }), + credentials: "include", + }); + } + async getGraphNames(datasetName) { const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs`; return fetch(url, { @@ -558,6 +568,47 @@ export class BackendConnection { }); } + async getCrossProfileDiagramRenderingDataForDataset(datasetName) { + let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/crossprofilediagramRendering`; + return await fetch(url, { + method: "GET", + mode: "cors", + headers: new Headers({ "Content-Type": "application/json" }), + credentials: "include", + }); + } + + async getCrossProfileDiagramForDataset(datasetName) { + let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/crossprofilediagram`; + return await fetch(url, { + method: "GET", + mode: "cors", + headers: new Headers({ "Content-Type": "application/json" }), + credentials: "include", + }); + } + + async getCrossProfileColorData(datasetName) { + let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/crossprofilediagramColors`; + return await fetch(url, { + method: "GET", + mode: "cors", + headers: new Headers({ "Content-Type": "application/json" }), + credentials: "include", + }); + } + + async putCrossProfileColorData(datasetName, colorData) { + let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/crossprofilediagramColors`; + return await fetch(url, { + method: "PUT", + mode: "cors", + headers: new Headers({ "Content-Type": "application/json" }), + body: JSON.stringify(colorData), + credentials: "include", + }); + } + async putCustomDiagram(datasetName, graphURI, diagramId, newDiagram) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/diagrams/${encodeURIComponent(diagramId)}`; return await fetch(url, { diff --git a/frontend/src/lib/rendering/mermaid/mermaidWrapper.svelte b/frontend/src/lib/rendering/mermaid/mermaidWrapper.svelte index d3e9ad0f..51e5a6be 100644 --- a/frontend/src/lib/rendering/mermaid/mermaidWrapper.svelte +++ b/frontend/src/lib/rendering/mermaid/mermaidWrapper.svelte @@ -48,7 +48,7 @@ */ window.getClassInformation = nodeId => { console.log("selecting class: ", nodeId); - editorState.selectedClassUUID.updateValue(nodeId); + editorState.selectedClass.updateValue(nodeId); }; }); diff --git a/frontend/src/lib/rendering/svelteflow/components/AssociationEdge.svelte b/frontend/src/lib/rendering/svelteflow/components/AssociationEdge.svelte index c3f93091..e68c869c 100644 --- a/frontend/src/lib/rendering/svelteflow/components/AssociationEdge.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/AssociationEdge.svelte @@ -23,16 +23,22 @@ useInternalNode, } from "@xyflow/svelte"; + import { userSettings } from "$lib/userSettings.svelte.js"; + import { getEdgeParams } from "./edgeUtils.ts"; let { id, source, target, data } = $props(); - - const style = "stroke-width: 2px; stroke: #000"; let markerEnd = data.useToAssociation ? "url(#associationTo)" : ""; let markerStart = data.useFromAssociation ? "url(#associationFrom)" : ""; let sourceNode = useInternalNode(source); let targetNode = useInternalNode(target); + let style = $derived( + userSettings.get("useColoredPropertiesInMergedView") && data.color + ? `stroke-width: 2px; stroke: ${data.color};` + : "stroke-width: 2px; stroke: #000;", + ); + let edgeParams = $derived.by(() => { if (sourceNode.current && targetNode.current) { return getEdgeParams(sourceNode.current, targetNode.current); diff --git a/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte b/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte index 8edb24f0..ed086652 100644 --- a/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte @@ -18,13 +18,14 @@
{#if attributes && attributes.length > 0} - {#each attributes as attr} -
- {attr.label}: {attr.type}  [{attr.multiplicity}] -
- {/each} + {#if isCrossProfileDiagram} + {#each groupByGraphURI(attributes) as group} +
+ {group.graphName} +
+ {#each group.props as attr} +
+ {attr.label}: {attr.type}  [{attr.multiplicity}] +
+ {/each} + {/each} + {:else} + {#each attributes as attr} +
+ {attr.label}: {attr.type}  [{attr.multiplicity}] +
+ {/each} + {/if} {:else if enumEntries && enumEntries.length > 0} - {#each enumEntries as enumEntry} -
- {enumEntry} -
- {/each} + {#if isCrossProfileDiagram} + {#each groupByGraphURI(enumEntries) as group} +
+ {group.graphName} +
+ {#each group.props as enumEntry} +
+ {enumEntry.label ?? enumEntry} +
+ {/each} + {/each} + {:else} + {#each enumEntries as enumEntry} +
+ {enumEntry.label ?? enumEntry} +
+ {/each} + {/if} {/if}
diff --git a/frontend/src/lib/rendering/svelteflow/components/EdgeMarkers.svelte b/frontend/src/lib/rendering/svelteflow/components/EdgeMarkers.svelte index 96e144d8..bd33f4f5 100644 --- a/frontend/src/lib/rendering/svelteflow/components/EdgeMarkers.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/EdgeMarkers.svelte @@ -41,7 +41,7 @@ markerHeight="10" orient="auto" > - + - + diff --git a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte index 8799ee34..7ab0223c 100644 --- a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte @@ -173,108 +173,110 @@ style={triggerStyle} {disabled} /> - - - Copy - - - Extend Class - - - { - showSHACLDialog = true; - }} - faIcon={faDiagramProject} - > - Constraints - - - + - Move - - - { - e.preventDefault(); - handleMoveToTop(); - }} - faIcon={faAnglesUp} - disabled={classActionsDisabled || isAtFront} - > - Move to front - - { - e.preventDefault(); - handleMoveUp(); - }} - faIcon={faAngleUp} - disabled={classActionsDisabled || isAtFront} - > - Move up - - + + Extend Class + + + { + showSHACLDialog = true; + }} + faIcon={faDiagramProject} + > + Constraints + + + - Layer - + Move + + + { + e.preventDefault(); + handleMoveToTop(); + }} + faIcon={faAnglesUp} + disabled={classActionsDisabled || isAtFront} + > + Move to front + + { + e.preventDefault(); + handleMoveUp(); + }} + faIcon={faAngleUp} + disabled={classActionsDisabled || isAtFront} + > + Move up + + + Layer + + { + e.preventDefault(); + handleMoveDown(); + }} + faIcon={faAngleDown} + disabled={classActionsDisabled || isAtBack} + > + Move down + + { + e.preventDefault(); + handleMoveToBottom(); + }} + faIcon={faAnglesDown} + disabled={classActionsDisabled || isAtBack} + > + Move to bottom + + + + + {#if editorState.selectedDiagram.getProperty("type") !== DiagramType.PACKAGE} { - e.preventDefault(); - handleMoveDown(); - }} - faIcon={faAngleDown} - disabled={classActionsDisabled || isAtBack} + onSelect={openRemoveFromDiagramDialog} + faIcon={faMinus} + variant="danger" > - Move down + Remove from Diagram - { - e.preventDefault(); - handleMoveToBottom(); - }} - faIcon={faAnglesDown} - disabled={classActionsDisabled || isAtBack} - > - Move to bottom - - - - - {#if editorState.selectedDiagram.getProperty("type") !== DiagramType.PACKAGE} + {/if} - Remove from Diagram + Delete class - {/if} - - Delete class - - + + {/if} - - - Add class - - {#if editorState.selectedDiagram.getProperty("type") === DiagramType.PACKAGE} + {#if editorState.selectedDiagram.getProperty("type") === DiagramType.PACKAGE} + + + Add class + + @@ -173,8 +174,8 @@ - {/if} - + + {/if} { - editorState.selectedClassUUID.subscribe(); - const selectedUUID = editorState.selectedClassUUID.getValue(); + editorState.selectedClass.subscribe(); + const selectedUUID = editorState.selectedClass.getProperty("id"); untrack(() => { if (!selectedUUID) { resetTemporaryFront(); @@ -302,7 +304,7 @@ bringToFrontTemporarily(id); - if (!editorState.selectedClassUUID.getValue()) { + if (!editorState.selectedClass.getProperty("id")) { eventStack.executeNewestEvent(id); editorState.selectedClassDataset.updateValue( editorState.selectedDataset.getValue(), @@ -310,12 +312,25 @@ editorState.selectedClassGraph.updateValue( nodeClickEvent.node.data.graphUri, ); - editorState.selectedClassUUID.updateValue(id); + const classType = + editorState.selectedDiagram.getProperty("type") === + DiagramType.CROSS_PROFILE + ? ClassType.MERGED_CLASS + : ClassType.SINGLE_CLASS; + editorState.selectedClass.updateValue({ + type: classType, + id: id, + }); } else { eventStack.executeNewestEvent({ datasetName: editorState.selectedDataset.getValue(), graphUri: nodeClickEvent.node.data.graphUri, classUuid: id, + classType: + editorState.selectedDiagram.getProperty("type") === + DiagramType.CROSS_PROFILE + ? ClassType.MERGED_CLASS + : ClassType.SINGLE_CLASS, }); } @@ -485,6 +500,7 @@ } const diagramUUID = editorState.selectedDiagram.getProperty("id"); + if (!diagramUUID) return; if (editorState.selectedGraph.getValue()) { bec.updateClassPositions( diff --git a/frontend/src/lib/sharedState.svelte.js b/frontend/src/lib/sharedState.svelte.js index 8f02e752..9f0a4311 100644 --- a/frontend/src/lib/sharedState.svelte.js +++ b/frontend/src/lib/sharedState.svelte.js @@ -37,9 +37,15 @@ import { export const DiagramType = { CUSTOM_GRAPH_DIAGRAM: "customGraphDiagram", CUSTOM_DATASET_DIAGRAM: "customDatasetDiagram", + CROSS_PROFILE: "crossProfile", PACKAGE: "package", }; +export const ClassType = { + SINGLE_CLASS: "singleClass", + MERGED_CLASS: "mergedClass", +}; + /** * The editorState object contains the state of the editor. Content might expand in the future. * @type {{ @@ -48,7 +54,7 @@ export const DiagramType = { * selectedDiagram: StateObjectPair, * selectedClassDataset: StateValuePair, * selectedClassGraph: StateValuePair, - * selectedClassUUID: StateValuePair, + * selectedClass: StateObjectPair, * focusedClassUUID: StateValuePair, * selectedContext: StateValuePair, * reset: () => void @@ -61,7 +67,7 @@ export const editorState = { selectedDiagram: new StateObjectPair({ type: null, id: null }), selectedClassDataset: new StateValuePair(), selectedClassGraph: new StateValuePair(), - selectedClassUUID: new StateValuePair(), + selectedClass: new StateObjectPair({ type: null, id: null }), focusedClassUUID: new StateValuePair(), selectedContext: new StateValuePair(), @@ -71,7 +77,7 @@ export const editorState = { this.selectedDiagram.updateValue({ type: null, id: null }); this.selectedClassDataset.updateValue(null); this.selectedClassGraph.updateValue(null); - this.selectedClassUUID.updateValue(null); + this.selectedClass.updateValue({ type: null, id: null }); this.focusedClassUUID.updateValue(null); this.selectedContext.updateValue(null); }, diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index f22cf74d..5b549839 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -70,7 +70,7 @@ $effect(async () => { editorState.selectedDiagram.subscribe(); - editorState.selectedClassUUID.subscribe(); + editorState.selectedClass.subscribe(); editorState.selectedGraph.subscribe(); editorState.selectedDataset.subscribe(); forceReloadTrigger.subscribe(); @@ -94,7 +94,7 @@ return; } forceReloadTrigger.trigger(); - editorState.selectedClassUUID.trigger(); + editorState.selectedClass.trigger(); editorState.selectedDiagram.trigger(); isDatasetReadOnly = false; } @@ -132,7 +132,7 @@ await fetchUndoRedo(); editorState.selectedDataset.trigger(); editorState.selectedGraph.trigger(); - editorState.selectedClassUUID.trigger(); + editorState.selectedClass.trigger(); forceReloadTrigger.trigger(); } diff --git a/frontend/src/routes/GraphDeleteDialog.svelte b/frontend/src/routes/GraphDeleteDialog.svelte index 1118de46..12511866 100644 --- a/frontend/src/routes/GraphDeleteDialog.svelte +++ b/frontend/src/routes/GraphDeleteDialog.svelte @@ -68,7 +68,7 @@ }); editorState.selectedClassDataset.updateValue(null); editorState.selectedClassGraph.updateValue(null); - editorState.selectedClassUUID.updateValue(null); + editorState.selectedClass.updateValue({ type: null, id: null }); toastStore.success( "Schema deleted", `"${deletedGraph}" was removed.`, diff --git a/frontend/src/routes/ImportDialog.svelte b/frontend/src/routes/ImportDialog.svelte index 3a330afd..987ce4ed 100644 --- a/frontend/src/routes/ImportDialog.svelte +++ b/frontend/src/routes/ImportDialog.svelte @@ -241,7 +241,7 @@ editorState.selectedDiagram.updateValue({ type: null, id: null }); editorState.selectedClassDataset.updateValue(null); editorState.selectedClassGraph.updateValue(null); - editorState.selectedClassUUID.updateValue(null); + editorState.selectedClass.updateValue({ type: null, id: null }); const importedCount = importedGraphUris.length; const summary = `${importedCount} graph${importedCount === 1 ? "" : "s"} imported into "${datasetName}".`; diff --git a/frontend/src/routes/NewClassDialog.svelte b/frontend/src/routes/NewClassDialog.svelte index 00cd3d07..6de86d91 100644 --- a/frontend/src/routes/NewClassDialog.svelte +++ b/frontend/src/routes/NewClassDialog.svelte @@ -33,6 +33,7 @@ import { getPackageDisplayLabel } from "$lib/utils/package-label.js"; import { + ClassType, DiagramType, editorState, forceReloadTrigger, @@ -235,7 +236,10 @@ }); editorState.selectedClassDataset.updateValue(datasetNameLocal); editorState.selectedClassGraph.updateValue(graphURILocal); - editorState.selectedClassUUID.updateValue(uuid); + editorState.selectedClass.updateValue({ + type: ClassType.SINGLE_CLASS, + id: uuid, + }); toastStore.success( "Class created", `"${classNameLocal.value}" was added.`, @@ -258,7 +262,7 @@ editorState.selectedDataset.trigger(); editorState.selectedGraph.trigger(); editorState.selectedDiagram.trigger(); - editorState.selectedClassUUID.trigger(); + editorState.selectedClass.trigger(); } } diff --git a/frontend/src/routes/NewGraphDialog.svelte b/frontend/src/routes/NewGraphDialog.svelte index fa651d09..6c9512de 100644 --- a/frontend/src/routes/NewGraphDialog.svelte +++ b/frontend/src/routes/NewGraphDialog.svelte @@ -130,7 +130,7 @@ type: DiagramType.PACKAGE, id: "default", }); - editorState.selectedClassUUID.updateValue(null); + editorState.selectedClass.updateValue({ type: null, id: null }); toastStore.success( "Schema created", `"${graphURILocal}" was added to "${datasetNameLocal}".`, diff --git a/frontend/src/routes/Searchbar.svelte b/frontend/src/routes/Searchbar.svelte index 6347afc0..dba52f38 100644 --- a/frontend/src/routes/Searchbar.svelte +++ b/frontend/src/routes/Searchbar.svelte @@ -30,6 +30,7 @@ import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { URI } from "$lib/models/dto/index.ts"; import { + ClassType, DiagramType, editorState, forceReloadTrigger, @@ -77,10 +78,13 @@ searchResult.datasetName, ); editorState.selectedClassGraph.updateValue(searchResult.graphUri); - editorState.selectedClassUUID.updateValue(searchResult.uuid); + editorState.selectedClass.updateValue({ + type: ClassType.SINGLE_CLASS, + id: searchResult.uuid, + }); editorState.focusedClassUUID.updateValue(searchResult.uuid); } else if (searchResult.type === "PACKAGE") { - editorState.selectedClassUUID.updateValue(null); + editorState.selectedClass.updateValue({ type: null, id: null }); editorState.focusedClassUUID.updateValue(null); editorState.selectedDiagram.updateValue({ type: DiagramType.PACKAGE, @@ -91,9 +95,10 @@ searchResult.datasetName, ); editorState.selectedClassGraph.updateValue(searchResult.graphUri); - editorState.selectedClassUUID.updateValue( - searchResult.parentClassUUID, - ); + editorState.selectedClass.updateValue({ + type: ClassType.SINGLE_CLASS, + id: searchResult.parentClassUUID, + }); editorState.focusedClassUUID.updateValue( searchResult.parentClassUUID, ); diff --git a/frontend/src/routes/UserSettingDialog.svelte b/frontend/src/routes/UserSettingDialog.svelte index 36f3c76c..7a04e2df 100644 --- a/frontend/src/routes/UserSettingDialog.svelte +++ b/frontend/src/routes/UserSettingDialog.svelte @@ -29,6 +29,7 @@ usePackagePrefix: false, defaultExportFormat: supportedRDFMediaTypes[0].mimeType, showPackagePrefix: false, + useColoredPropertiesInMergedView: true, normalizeComments: true, }; @@ -70,11 +71,7 @@ - (localSettings["usePackagePrefix"] = true)} - callOnInputFalse={() => - (localSettings["usePackagePrefix"] = false)} + bind:value={localSettings["usePackagePrefix"]} labelFirst={false} /> - (localSettings["showPackagePrefix"] = true)} - callOnInputFalse={() => - (localSettings["showPackagePrefix"] = false)} + bind:value={localSettings["showPackagePrefix"]} + labelFirst={false} + /> + - (localSettings["normalizeComments"] = true)} - callOnInputFalse={() => - (localSettings["normalizeComments"] = false)} + bind:value={localSettings["normalizeComments"]} labelFirst={false} /> diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte index dcb4a4e3..c2f2f4b3 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -162,7 +162,7 @@ forceReloadTrigger.trigger(); editorState.selectedClassDataset.updateValue(null); editorState.selectedClassGraph.updateValue(null); - editorState.selectedClassUUID.updateValue(null); + editorState.selectedClass.updateValue({ type: null, id: null }); toastStore.success( `${type ? type.charAt(0).toUpperCase() + type.slice(1) : "Resource"} deleted`, label ? `"${label}" was removed.` : "Resource was removed.", diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index 33aa3bfb..a0ebe06f 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -109,7 +109,7 @@ let graphHasOntology = $derived(!!ontology); let disableCopyClassButton = $derived( - !editorState.selectedClassUUID.getValue(), + !editorState.selectedClass.getProperty("id"), ); let disablePasteButton = $derived( isDatasetReadOnly || @@ -122,7 +122,7 @@ $effect(async () => { editorState.selectedDiagram.subscribe(); - editorState.selectedClassUUID.subscribe(); + editorState.selectedClass.subscribe(); editorState.selectedGraph.subscribe(); editorState.selectedDataset.subscribe(); forceReloadTrigger.subscribe(); @@ -314,7 +314,7 @@ function copyClass() { copyState.classUUID.updateValue( - editorState.selectedClassUUID.getValue(), + editorState.selectedClass.getProperty("id"), ); copyState.graphURI.updateValue( editorState.selectedClassGraph.getValue(), diff --git a/frontend/src/routes/layout/menu-bar/View.svelte b/frontend/src/routes/layout/menu-bar/View.svelte index dbc265df..b51be16d 100644 --- a/frontend/src/routes/layout/menu-bar/View.svelte +++ b/frontend/src/routes/layout/menu-bar/View.svelte @@ -47,7 +47,7 @@ $effect(async () => { editorState.selectedDiagram.subscribe(); - editorState.selectedClassUUID.subscribe(); + editorState.selectedClass.subscribe(); editorState.selectedGraph.subscribe(); editorState.selectedDataset.subscribe(); forceReloadTrigger.subscribe(); diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index 6f9280fa..fa411a6e 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -83,6 +83,7 @@ let datasetOfClassToOpenNext = $state(null); let graphOfClassToOpenNext = $state(null); let classToOpenNext = $state(null); + let classTypeOfClassToOpenNext = $state(null); let pendingAction = $state(null); @@ -91,7 +92,7 @@ ); $effect(() => { - editorState.selectedClassUUID.subscribe(); + editorState.selectedClass.subscribe(); forceReloadTrigger.subscribe(); const cancellation = { cancelled: false }; @@ -163,10 +164,16 @@ } export function closeClassEditor( - { datasetName = null, graphUri = null, classUuid = null } = { + { + datasetName = null, + graphUri = null, + classUuid = null, + classType = null, + } = { datasetName: null, graphUri: null, classUuid: null, + classType: null, }, ) { if (!showDiscardSaveConfirmDialog && reactiveClass?.isModified) { @@ -174,11 +181,15 @@ datasetOfClassToOpenNext = datasetName; graphOfClassToOpenNext = graphUri; classToOpenNext = classUuid; + classTypeOfClassToOpenNext = classType; return; } editorState.selectedClassDataset.updateValue(datasetName); editorState.selectedClassGraph.updateValue(graphUri); - editorState.selectedClassUUID.updateValue(classUuid); + editorState.selectedClass.updateValue({ + type: classType, + id: classUuid, + }); } async function loadContext() { @@ -333,6 +344,7 @@ {datasetOfClassToOpenNext} {graphOfClassToOpenNext} {classToOpenNext} + {classTypeOfClassToOpenNext} {closeClassEditor} />
diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/save-association-to-backend.js b/frontend/src/routes/mainpage/classEditor/components/associations/save-association-to-backend.js index 47fe9c74..feb55c86 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/save-association-to-backend.js +++ b/frontend/src/routes/mainpage/classEditor/components/associations/save-association-to-backend.js @@ -48,7 +48,7 @@ export function saveApiAssociationToBackend( return { ok: false, errorText }; }) .finally(() => { - editorState.selectedClassUUID.trigger(); + editorState.selectedClass.trigger(); editorState.selectedDiagram.trigger(); }); } diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/save-attribute-to-backend.js b/frontend/src/routes/mainpage/classEditor/components/attributes/save-attribute-to-backend.js index 9cff6ca3..b38f747d 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/save-attribute-to-backend.js +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/save-attribute-to-backend.js @@ -44,7 +44,7 @@ export async function saveApiAttributeToBackend( console.error("Could not save attribute:", errorText); return { ok: false, errorText }; } finally { - editorState.selectedClassUUID.trigger(); + editorState.selectedClass.trigger(); editorState.selectedDiagram.trigger(); } } diff --git a/frontend/src/routes/mainpage/classEditor/components/enum-entries/save-enum-entry-to-backend.js b/frontend/src/routes/mainpage/classEditor/components/enum-entries/save-enum-entry-to-backend.js index efc641f7..c05139af 100644 --- a/frontend/src/routes/mainpage/classEditor/components/enum-entries/save-enum-entry-to-backend.js +++ b/frontend/src/routes/mainpage/classEditor/components/enum-entries/save-enum-entry-to-backend.js @@ -44,7 +44,7 @@ export async function saveApiEnumEntryToBackend( console.error("Could not save enum entry:", errorText); return { ok: false, errorText }; } finally { - editorState.selectedClassUUID.trigger(); + editorState.selectedClass.trigger(); editorState.selectedDiagram.trigger(); } } diff --git a/frontend/src/routes/mainpage/classEditor/mergedClassEditor.svelte b/frontend/src/routes/mainpage/classEditor/mergedClassEditor.svelte new file mode 100644 index 00000000..49ac088f --- /dev/null +++ b/frontend/src/routes/mainpage/classEditor/mergedClassEditor.svelte @@ -0,0 +1,102 @@ + + + + +{#if loading} +
+
+ +
+
+{:else if mergedClass && mergedClass.sources?.length > 0} +
+ +
+ + {#if activeSource} + {#key activeSource.classUuid + activeSource.graphUri} +
+ +
+ {/key} + {/if} +{:else} +

+ No sources available for this class. +

+{/if} diff --git a/frontend/src/routes/mainpage/mermaidWrapper.svelte b/frontend/src/routes/mainpage/mermaidWrapper.svelte index 27b103f7..d544e288 100644 --- a/frontend/src/routes/mainpage/mermaidWrapper.svelte +++ b/frontend/src/routes/mainpage/mermaidWrapper.svelte @@ -84,7 +84,7 @@ */ window.getClassInformation = nodeId => { console.log("selecting class: ", nodeId); - if (!editorState.selectedClassUUID.getValue()) { + if (!editorState.selectedClass.getProperty("id")) { eventStack.executeNewestEvent(nodeId); editorState.selectedClassDataset.updateValue( editorState.selectedDataset.getValue(), @@ -92,7 +92,7 @@ editorState.selectedClassGraph.updateValue( editorState.selectedGraph.getValue(), ); - editorState.selectedClassUUID.updateValue(nodeId); + editorState.selectedClass.updateValue(nodeId); return; } eventStack.executeNewestEvent({ diff --git a/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte b/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte index c2e09c73..5c4d4ff8 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte @@ -34,6 +34,7 @@ DiagramType, copyState, editorState, + ClassType, } from "$lib/sharedState.svelte.js"; import { shortenIri } from "$lib/utils/iri.js"; @@ -54,6 +55,8 @@ namespaces = [], readonly = false, onPackChange = () => {}, + classType = ClassType.SINGLE_CLASS, + diagramType = DiagramType.PACKAGE, } = $props(); let showDeleteDependenciesDialog = $state(false); @@ -78,18 +81,22 @@ classNavEntry.parent?.open(); } onPackChange(); - if (!editorState.selectedClassUUID.getValue()) { + if (!editorState.selectedClass.getProperty("id")) { eventStack.executeNewestEvent(classNavEntry.id); editorState.selectedClassDataset.updateValue(datasetNavEntry.id); editorState.selectedClassGraph.updateValue(graphNavEntry.id); - editorState.selectedClassUUID.updateValue(classNavEntry.id); + editorState.selectedClass.updateValue({ + type: classType, + id: classNavEntry.id, + }); return; } //The event executed to open the discard confirm delete dialog eventStack.executeNewestEvent({ datasetName: datasetNavEntry.id, - graphUri: graphNavEntry.id, + graphUri: graphNavEntry?.id ?? null, classUuid: classNavEntry.id, + classType: classType, }); } @@ -104,8 +111,9 @@ function showClassInPackage() { editorState.selectedDataset.updateValue(datasetNavEntry.id); editorState.selectedGraph.updateValue(graphNavEntry.id); + //editorState.selectedClassType.updateValue(classType); editorState.selectedDiagram.updateValue({ - type: DiagramType.PACKAGE, + type: diagramType, id: classNavEntry.parent?.id ?? "default", }); selectClass(); @@ -136,78 +144,82 @@ /> - - Copy - - + {#if classType === ClassType.SINGLE_CLASS} + + Copy + + + {/if} Show in diagram - { - showSHACLDialog = true; - }} - faIcon={faDiagramProject} - > - Constraints - - - { - showExtendClassDialog = true; - }} - faIcon={faFileExport} - > - Extend Class - - {#if !diagramId} + {#if classType === ClassType.SINGLE_CLASS} { - showAddToGraphDiagramDialog = true; + showSHACLDialog = true; }} - faIcon={faObjectGroup} + faIcon={faDiagramProject} > - Add to Profile Diagram + Constraints + { - showAddToDatasetDiagramDialog = true; + showExtendClassDialog = true; }} - faIcon={faObjectGroup} + faIcon={faFileExport} > - Add to Dataset Diagram + Extend Class - {/if} - - {#if diagramId} + {#if !diagramId} + { + showAddToGraphDiagramDialog = true; + }} + faIcon={faObjectGroup} + > + Add to Profile Diagram + + { + showAddToDatasetDiagramDialog = true; + }} + faIcon={faObjectGroup} + > + Add to Dataset Diagram + + {/if} + + {#if diagramId} + { + showRemoveFromDiagramDialog = true; + }} + faIcon={faMinus} + variant="danger" + > + Remove from Diagram + + {/if} { - showRemoveFromDiagramDialog = true; + selectClass(); + showDeleteDependenciesDialog = true; }} - faIcon={faMinus} + disabled={readonly} + faIcon={faTrash} variant="danger" > - Remove from Diagram + Delete Class {/if} - { - selectClass(); - showDeleteDependenciesDialog = true; - }} - disabled={readonly} - faIcon={faTrash} - variant="danger" - > - Delete Class - diff --git a/frontend/src/routes/mainpage/packageNavigation/CrossProfileDiagramsSection.svelte b/frontend/src/routes/mainpage/packageNavigation/CrossProfileDiagramsSection.svelte new file mode 100644 index 00000000..6329393e --- /dev/null +++ b/frontend/src/routes/mainpage/packageNavigation/CrossProfileDiagramsSection.svelte @@ -0,0 +1,125 @@ + + + + + + + + + 0} + expanded={isOpen} + isSelected={isMergedViewSelected} + onclick={() => { + selectMergedView(); + }} + onToggle={() => (isOpen = !isOpen)} + /> + + + { + showColorDialog = true; + }} + faIcon={faFileExport} + > + Edit Schema Colors + + + + +{#if isOpen && classes.length > 0} +
+ {#each classes as cls (cls.uuid)} + selectMergedView(), + }, + }} + classType={ClassType.MERGED_CLASS} + diagramType={DiagramType.CROSS_PROFILE} + readonly={true} + /> + {/each} +
+{/if} + + diff --git a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte index 7d348f75..bd8911a9 100644 --- a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte @@ -27,18 +27,23 @@ faDiagramProject, faPlus, } from "@fortawesome/free-solid-svg-icons"; - import { getContext } from "svelte"; + import { getContext, onMount } from "svelte"; import { enableEditing, disableEditing, } from "$lib/actions/editingActions.js"; import { getNamespaces, isReadOnly } from "$lib/api/apiDatasetUtils.js"; + import { BackendConnection } from "$lib/api/backend.js"; import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; - import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; - import { editorState } from "$lib/sharedState.svelte.js"; + import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; + import { + editorState, + forceReloadTrigger, + } from "$lib/sharedState.svelte.js"; + import CrossProfileDiagramsSection from "./CrossProfileDiagramsSection.svelte"; import CustomDiagramsSection from "./CustomDiagramsSection.svelte"; import GraphSection from "./GraphSection.svelte"; import { isSelectedDataset } from "./packageNavigationUtils.svelte.js"; @@ -51,6 +56,8 @@ let { datasetNavEntry } = $props(); + const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); + let showImportDialog = $state(false); let showNewGraphDialog = $state(false); let showNewDiagramDialog = $state(false); @@ -78,6 +85,11 @@ wasDatasetSelected = isDatasetSelected; }); + onMount(async () => { + let res = await bec.getCrossProfileID(datasetNavEntry.label); + datasetNavEntry.crossProfileID = await res.text(); + }); + async function fetchNamespaces() { if (!datasetNavEntry?.label) { namespaces = []; @@ -238,6 +250,8 @@ /> {/each} + + + + + + +
+
+ {#if colorEntries.length === 0} +

+ No graphs available for this dataset. +

+ {:else} +

+ Assign a color to each schema. The color is used in the + merged view. +

+ +
+ {#each colorEntries as entry (entry.graphURI)} +
+ + +
+

+ {shortName(entry.graphURI)} +

+

+ {entry.graphURI} +

+
+ + +
+ {/each} +
+ {/if} +
+
+
diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte index 60486ae7..6ac90152 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte @@ -66,7 +66,7 @@ }); editorState.selectedClassDataset.updateValue(null); editorState.selectedClassGraph.updateValue(null); - editorState.selectedClassUUID.updateValue(null); + editorState.selectedClass.updateValue({ type: null, id: null }); } toastStore.success( "Custom diagram deleted", diff --git a/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js b/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js index 70f990bc..4f57a5ab 100644 --- a/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js +++ b/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js @@ -55,7 +55,7 @@ export function isSelectedClass(dataset, graph, cls) { const datasetLabel = dataset?.label ?? dataset; const graphUri = graph ? getUri(graph) : null; return ( - editorState.selectedClassUUID.getValue() === cls.uuid && + editorState.selectedClass.getProperty("id") === cls.uuid && editorState.selectedClassDataset.getValue() === datasetLabel && editorState.selectedClassGraph.getValue() === graphUri ); diff --git a/frontend/src/routes/mainpage/packageNavigation/save-copy-class-to-backend.js b/frontend/src/routes/mainpage/packageNavigation/save-copy-class-to-backend.js index 12e0a357..7d3d30ad 100644 --- a/frontend/src/routes/mainpage/packageNavigation/save-copy-class-to-backend.js +++ b/frontend/src/routes/mainpage/packageNavigation/save-copy-class-to-backend.js @@ -19,6 +19,7 @@ import { BackendConnection } from "$lib/api/backend.js"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { + ClassType, copyState, DiagramType, editorState, @@ -64,7 +65,10 @@ export async function saveCopyClass( type: DiagramType.PACKAGE, id: packageDTO?.uuid ?? "default", }); - editorState.selectedClassUUID.updateValue(uuid); + editorState.selectedClass.updateValue({ + type: ClassType.SINGLE_CLASS, + id: uuid, + }); toastStore.success("Class pasted", `"${name}" was pasted.`); } else { const errorText = await res.text(); diff --git a/frontend/src/routes/mainpage/packageWindow.svelte b/frontend/src/routes/mainpage/packageWindow.svelte index d4fd5843..f34c240e 100644 --- a/frontend/src/routes/mainpage/packageWindow.svelte +++ b/frontend/src/routes/mainpage/packageWindow.svelte @@ -18,13 +18,15 @@ @@ -157,7 +160,7 @@
diff --git a/frontend/src/routes/shacl/shaclclassspecific/SHACLShapeTtlRenderer.svelte b/frontend/src/routes/shacl/shaclclassspecific/SHACLShapeTtlRenderer.svelte index 5c542fec..5255f8c8 100644 --- a/frontend/src/routes/shacl/shaclclassspecific/SHACLShapeTtlRenderer.svelte +++ b/frontend/src/routes/shacl/shaclclassspecific/SHACLShapeTtlRenderer.svelte @@ -86,7 +86,9 @@ "/graphs/" + encodeURIComponent(classGraphUri) + "/classes/" + - encodeURIComponent(editorState.selectedClassUUID.getValue()) + + encodeURIComponent( + editorState.selectedClass.getProperty("id"), + ) + "/shacl/custom", { method: "PUT", @@ -116,7 +118,7 @@ ); }) .finally(() => { - editorState.selectedClassUUID.trigger(); + editorState.selectedClass.trigger(); }); }