diff --git a/lib/Service/ArchiMateExportService.php b/lib/Service/ArchiMateExportService.php index 02ea7c86..a1e33eba 100644 --- a/lib/Service/ArchiMateExportService.php +++ b/lib/Service/ArchiMateExportService.php @@ -86,15 +86,26 @@ public function arrayToXml(array $data, \SimpleXMLElement $xml): \SimpleXMLEleme if (str_starts_with($attrKey, ':') || in_array($attrKey, $addedAttributes)) { continue; } - + // Fix double underscores to colons $cleanAttrKey = str_replace('__', ':', $attrKey); - + // Skip if cleaned version was already added if (in_array($cleanAttrKey, $addedAttributes)) { continue; } - + + // Handle namespaced attributes (e.g., xml:lang, xsi:type) + [$nsPrefix, $local] = $this->splitNamespacedKey($cleanAttrKey); + if ($nsPrefix !== null) { + $nsUri = $this->getNamespaceUri($xml, $nsPrefix); + if ($nsUri) { + $xml->addAttribute($nsPrefix . ':' . $local, (string) $attrValue, $nsUri); + $addedAttributes[] = $nsPrefix . ':' . $local; + continue; + } + } + $xml->addAttribute($cleanAttrKey, (string) $attrValue); $addedAttributes[] = $cleanAttrKey; } @@ -177,6 +188,13 @@ private function splitNamespacedKey(string $key): array return [$parts[0], $parts[1]]; } } + // Also handle already-converted colon notation (e.g., 'xml:lang') + if (str_contains($key, ':')) { + $parts = explode(':', $key, 2); + if (count($parts) === 2 && $parts[0] !== '' && $parts[1] !== '') { + return [$parts[0], $parts[1]]; + } + } return [null, $key]; } @@ -233,6 +251,16 @@ private function filterProblematicFields(array $data, array $fieldsToRemove): ar private function getNamespaceUri(\SimpleXMLElement $xml, string $prefix): string { + // Well-known namespaces — check first to avoid expensive getDocNamespaces calls + static $wellKnown = [ + 'xml' => 'http://www.w3.org/XML/1998/namespace', + 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + ]; + + if (isset($wellKnown[$prefix])) { + return $wellKnown[$prefix]; + } + $namespaces = $xml->getDocNamespaces(true) ?: []; return $namespaces[$prefix] ?? ''; } @@ -731,7 +759,7 @@ private function generateXmlDirectly(array $objects, array $schemaIdMap): string } // Step 2: Generate XML sections directly - $validSections = ['elements', 'relationships', 'views', 'organizations', 'property_definitions']; + $validSections = ['elements', 'relationships', 'organizations', 'property_definitions', 'views']; $sectionCounts = []; // Map singular section names to plural for XML generation @@ -754,15 +782,45 @@ private function generateXmlDirectly(array $objects, array $schemaIdMap): string if (!empty($sectionObjects)) { $sectionCounts[$sectionName] = count($sectionObjects); - + + // Organizations are stored as a single tree object with the full hierarchy + // in the xml field. Write items directly as children of . + if ($sectionName === 'organizations') { + $orgFolder = $this->createSectionFolder($xml, $sectionName); + foreach ($sectionObjects as $object) { + if (is_object($object) && method_exists($object, 'jsonSerialize')) { + $object = $object->jsonSerialize(); + } + $xmlField = $object['xml'] ?? []; + // The xml field contains the raw organizations data with 'item' array + if (isset($xmlField['item'])) { + $items = $xmlField['item']; + // Ensure items is a list (could be single assoc array for one top-level folder) + if (!isset($items[0])) { + $items = [$items]; + } + foreach ($items as $itemData) { + if (is_array($itemData)) { + $itemNode = $orgFolder->addChild('item'); + $this->addOrganizationItemToXml($itemNode, $itemData); + } + } + } + } + $this->logger->debug("Generated XML section: {$sectionName} (tree mode)", [ + 'object_count' => count($sectionObjects) + ]); + continue; + } + // Create section folder $sectionFolder = $this->createSectionFolder($xml, $sectionName); - + // Add all objects in this section foreach ($sectionObjects as $object) { $this->addObjectDirectlyToXmlWithProperties($sectionFolder, $object, $sectionName, $propertyDefinitionMap); } - + $this->logger->debug("Generated XML section: {$sectionName}", [ 'object_count' => count($sectionObjects) ]); @@ -848,6 +906,15 @@ private function addViewDataToXmlNode(\SimpleXMLElement $viewNode, array $viewDa if (str_starts_with($attrKey, ':')) { continue; // Skip duplicate attributes with colon prefix } + // Handle namespaced attributes (e.g., xsi:type) + [$nsPrefix, $local] = $this->splitNamespacedKey($attrKey); + if ($nsPrefix !== null) { + $nsUri = $this->getNamespaceUri($viewNode, $nsPrefix); + if ($nsUri) { + $viewNode->addAttribute($nsPrefix . ':' . $local, (string)$attrValue, $nsUri); + continue; + } + } $viewNode->addAttribute($attrKey, (string)$attrValue); } } @@ -866,52 +933,37 @@ private function addViewDataToXmlNode(\SimpleXMLElement $viewNode, array $viewDa } } - // Process all other elements, handling nodes and connections specially - foreach ($viewData as $key => $value) { - // Skip already processed attributes and metadata, including duplicate id element - if (in_array($key, ['_attributes', '_identifier', 'identifier', '_xsi__type', 'xsi:type', '_xsi:type', 'id', '_essential_data', 'viewNodes', 'viewRelationships'])) { - continue; - } - // Skip colon-prefixed duplicate keys (artifacts from XML-to-JSON parsing) - if (is_string($key) && str_starts_with($key, ':')) { - continue; - } - - if ($key === 'node' && is_array($value)) { - // Handle view nodes with specialized processing - foreach ($value as $nodeData) { - if (is_array($nodeData)) { - $nodeElement = $viewNode->addChild('node'); - $this->addNodeDataToXmlElement($nodeElement, $nodeData); - } else { - // If it's not an array, treat it as a simple text value - $nodeElement = $viewNode->addChild('node'); - $nodeElement[0] = (string)$nodeData; - } - } - } elseif ($key === 'connection' && is_array($value)) { - // Handle view connections with special processing - foreach ($value as $connectionData) { - if (is_array($connectionData)) { - $connectionElement = $viewNode->addChild('connection'); - $this->arrayToXml($connectionData, $connectionElement); - } else { - // If it's not an array, treat it as a simple text value - $connectionElement = $viewNode->addChild('connection'); - $connectionElement[0] = (string)$connectionData; - } + // XSD-required order for ViewType (Diagram): name → documentation → properties → node → connection + $this->addLangTextChild($viewNode, 'name', $viewData['name'] ?? null); + $this->addLangTextChild($viewNode, 'documentation', $viewData['documentation'] ?? null); + if (isset($viewData['properties']) && is_array($viewData['properties'])) { + $this->addPropertiesToXml($viewNode, $viewData['properties']); + } + + // Nodes + if (isset($viewData['node']) && is_array($viewData['node'])) { + $nodes = $viewData['node']; + if (!$this->isList($nodes)) { + $nodes = [$nodes]; + } + foreach ($nodes as $nodeData) { + if (is_array($nodeData)) { + $nodeElement = $viewNode->addChild('node'); + $this->addNodeDataToXmlElement($nodeElement, $nodeData); } - } else { - // Handle all other elements normally (name, documentation, properties, etc.) - if ($key === 'properties' && is_array($value)) { - // Use specialized property handling to avoid duplicate attributes - $this->addPropertiesToXml($viewNode, $value); - } elseif (is_array($value)) { - $childElement = $viewNode->addChild($key); - $this->arrayToXml($value, $childElement); - } else { - $childElement = $viewNode->addChild($key); - $childElement[0] = (string)$value; + } + } + + // Connections + if (isset($viewData['connection']) && is_array($viewData['connection'])) { + $connections = $viewData['connection']; + if (!$this->isList($connections)) { + $connections = [$connections]; + } + foreach ($connections as $connectionData) { + if (is_array($connectionData)) { + $connectionElement = $viewNode->addChild('connection'); + $this->arrayToXml($connectionData, $connectionElement); } } } @@ -933,12 +985,24 @@ private function addNodeDataToXmlElement(\SimpleXMLElement $nodeElement, array $ '_xsi__type' => 'xsi:type' ]; + $addedNodeAttrs = []; foreach ($nodeAttributes as $dataKey => $xmlAttr) { if (isset($nodeData[$dataKey])) { + // Handle namespaced attributes like xsi:type + [$nsPrefix, $local] = $this->splitNamespacedKey($xmlAttr); + if ($nsPrefix !== null) { + $nsUri = $this->getNamespaceUri($nodeElement, $nsPrefix); + if ($nsUri) { + $nodeElement->addAttribute($nsPrefix . ':' . $local, (string)$nodeData[$dataKey], $nsUri); + $addedNodeAttrs[] = $nsPrefix . ':' . $local; + continue; + } + } $nodeElement->addAttribute($xmlAttr, (string)$nodeData[$dataKey]); + $addedNodeAttrs[] = $xmlAttr; } } - + // Also check regular attributes array if (isset($nodeData['_attributes'])) { foreach ($nodeData['_attributes'] as $attrKey => $attrValue) { @@ -946,52 +1010,123 @@ private function addNodeDataToXmlElement(\SimpleXMLElement $nodeElement, array $ continue; // Skip duplicate attributes with colon prefix } // Skip if we already added this attribute from the direct keys - if (in_array($attrKey, ['identifier', 'x', 'y', 'w', 'h', 'elementRef', 'xsi:type'])) { + if (in_array($attrKey, ['identifier', 'x', 'y', 'w', 'h', 'elementRef', 'xsi:type']) || in_array($attrKey, $addedNodeAttrs)) { continue; } + // Handle namespaced attributes + [$nsPrefix, $local] = $this->splitNamespacedKey($attrKey); + if ($nsPrefix !== null) { + $nsUri = $this->getNamespaceUri($nodeElement, $nsPrefix); + if ($nsUri) { + $nodeElement->addAttribute($nsPrefix . ':' . $local, (string)$attrValue, $nsUri); + continue; + } + } $nodeElement->addAttribute($attrKey, (string)$attrValue); } } - // Process nested elements (style, label, properties, etc.) but skip the attribute keys and duplicate keys - $skipKeys = array_keys($nodeAttributes); - $skipKeys[] = '_attributes'; - // Also skip the triple underscore duplicates - $skipKeys = array_merge($skipKeys, ['___identifier', '___x', '___y', '___w', '___h', '___elementRef']); - - foreach ($nodeData as $key => $value) { - if (in_array($key, $skipKeys)) { - continue; // Skip already processed attributes + // XSD-required order for ViewNodeType: label → style → viewRef → node (nested) + // Label (used in Label nodes) + if (isset($nodeData['label'])) { + $labelData = $nodeData['label']; + if (is_array($labelData)) { + $labelElement = $nodeElement->addChild('label'); + $this->arrayToXml($labelData, $labelElement); + } else { + $labelElement = $nodeElement->addChild('label'); + $labelElement[0] = (string)$labelData; } + } - // Skip numeric keys as they can't be valid XML element names - if (is_numeric($key)) { - continue; + // Style (lineColor → fillColor → font per XSD StyleType order) + if (isset($nodeData['style']) && is_array($nodeData['style'])) { + $styleElement = $nodeElement->addChild('style'); + $styleData = $nodeData['style']; + // Enforce StyleType order: lineColor → fillColor → font + foreach (['lineColor', 'fillColor', 'font'] as $styleKey) { + if (isset($styleData[$styleKey]) && is_array($styleData[$styleKey])) { + $child = $styleElement->addChild($styleKey); + $this->arrayToXml($styleData[$styleKey], $child); + } } + } - // Skip colon-prefixed duplicate keys (artifacts from XML-to-JSON parsing) - if (is_string($key) && str_starts_with($key, ':')) { - continue; + // viewRef + if (isset($nodeData['viewRef'])) { + $viewRefData = $nodeData['viewRef']; + if (is_array($viewRefData)) { + $vrElement = $nodeElement->addChild('viewRef'); + $this->arrayToXml($viewRefData, $vrElement); } - - if ($key === 'node' && is_array($value)) { - // Handle nested nodes recursively - foreach ($value as $nestedNodeData) { - if (is_array($nestedNodeData)) { - $nestedNodeElement = $nodeElement->addChild('node'); - $this->addNodeDataToXmlElement($nestedNodeElement, $nestedNodeData); - } else { - // If it's not an array, treat it as a simple text value - $nestedNodeElement = $nodeElement->addChild('node'); - $nestedNodeElement[0] = (string)$nestedNodeData; + } + + // Nested nodes (Container/Element type) + if (isset($nodeData['node']) && is_array($nodeData['node'])) { + $nestedNodes = $nodeData['node']; + if (!$this->isList($nestedNodes)) { + $nestedNodes = [$nestedNodes]; + } + foreach ($nestedNodes as $nestedNodeData) { + if (is_array($nestedNodeData)) { + $nestedNodeElement = $nodeElement->addChild('node'); + $this->addNodeDataToXmlElement($nestedNodeElement, $nestedNodeData); + } + } + } + } + + /** + * Add organization item to XML with XSD-required child order: label → documentation → item + */ + private function addOrganizationItemToXml(\SimpleXMLElement $itemNode, array $itemData): void + { + // Add identifierRef attribute if present + if (isset($itemData['_identifierRef'])) { + $itemNode->addAttribute('identifierRef', (string)$itemData['_identifierRef']); + } elseif (isset($itemData['_attributes']['identifierRef'])) { + $itemNode->addAttribute('identifierRef', (string)$itemData['_attributes']['identifierRef']); + } + + // XSD order: label → documentation → item + // Labels first + if (isset($itemData['label'])) { + $labels = $itemData['label']; + if (is_array($labels) && !$this->isList($labels)) { + $labels = [$labels]; // Single label → list + } + if (is_array($labels)) { + foreach ($labels as $labelData) { + if (is_array($labelData)) { + $labelElement = $itemNode->addChild('label'); + $this->arrayToXml($labelData, $labelElement); + } elseif (is_string($labelData)) { + $labelElement = $itemNode->addChild('label'); + $labelElement[0] = $labelData; + } + } + } elseif (is_string($labels)) { + $labelElement = $itemNode->addChild('label'); + $labelElement[0] = $labels; + } + } + + // Documentation + $this->addLangTextChild($itemNode, 'documentation', $itemData['documentation'] ?? null); + + // Nested items + if (isset($itemData['item'])) { + $items = $itemData['item']; + if (is_array($items) && !$this->isList($items)) { + $items = [$items]; + } + if (is_array($items)) { + foreach ($items as $childItemData) { + if (is_array($childItemData)) { + $childNode = $itemNode->addChild('item'); + $this->addOrganizationItemToXml($childNode, $childItemData); } } - } elseif (is_array($value)) { - $childElement = $nodeElement->addChild($key); - $this->arrayToXml($value, $childElement); - } else { - $childElement = $nodeElement->addChild($key); - $childElement[0] = (string)$value; } } } @@ -1081,7 +1216,7 @@ private function addCleanDataToXmlNode(\SimpleXMLElement $node, array $data, ?st } else { $attributes['xsi:type'] = (string)$attrValue; } - } elseif (in_array($attrKey, ['identifier', 'source', 'target', 'accessType', 'type'])) { + } elseif (in_array($attrKey, ['identifier', 'source', 'target', 'accessType', 'isDirected', 'type'])) { if ($attrKey === 'type' && !$isPropertyDefinition) { $attributes['xsi:type'] = (string)$attrValue; } else { @@ -1102,7 +1237,7 @@ private function addCleanDataToXmlNode(\SimpleXMLElement $node, array $data, ?st } } } - foreach (['source', 'target', 'accessType', 'type'] as $attrName) { + foreach (['source', 'target', 'accessType', 'isDirected', 'type'] as $attrName) { if (isset($data[$attrName]) && !isset($attributes[$attrName])) { $isPropertyDefinition = ($sectionName === 'property_definitions'); if ($attrName === 'type') { @@ -1123,51 +1258,12 @@ private function addCleanDataToXmlNode(\SimpleXMLElement $node, array $data, ?st $node->addAttribute($attrName, $attrValue); } } - // Handle child elements - foreach ($data as $key => $value) { - if (in_array($key, ['identifier', 'xsi:type', 'xsi_type', '_xsi:type', '_type', 'source', 'target', 'accessType', 'type', '_attributes', '_essential_data'])) { - continue; - } - // Skip colon-prefixed duplicate keys (artifacts from XML-to-JSON parsing) - if (is_string($key) && str_starts_with($key, ':')) { - continue; - } - if ($key === 'name' && is_array($value)) { - $nameNode = $node->addChild('name'); - if (isset($value['_value'])) { - $nameNode[0] = (string)$value['_value']; - } - foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) { - if (isset($value[$langKey])) { - $nameNode->addAttribute('xml:lang', $value[$langKey], 'http://www.w3.org/XML/1998/namespace'); - break; - } - } - } elseif ($key === 'documentation' && is_array($value)) { - $docNode = $node->addChild('documentation'); - if (isset($value['_value'])) { - $docNode[0] = (string)$value['_value']; - } - foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) { - if (isset($value[$langKey])) { - $docNode->addAttribute('xml:lang', $value[$langKey], 'http://www.w3.org/XML/1998/namespace'); - break; - } - } - } elseif ($key === 'properties' && is_array($value)) { - $this->addPropertiesToXml($node, $value); - } elseif ($key === 'value' && is_array($value)) { - $valueNode = $node->addChild('value'); - if (isset($value['_value'])) { - $valueNode[0] = (string)$value['_value']; - } - foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) { - if (isset($value[$langKey])) { - $valueNode->addAttribute('xml:lang', $value[$langKey], 'http://www.w3.org/XML/1998/namespace'); - break; - } - } - } + // Handle child elements in XSD-required order (xs:sequence): + // NamedReferenceableType: name → documentation → properties + $this->addLangTextChild($node, 'name', $data['name'] ?? null); + $this->addLangTextChild($node, 'documentation', $data['documentation'] ?? null); + if (isset($data['properties']) && is_array($data['properties'])) { + $this->addPropertiesToXml($node, $data['properties']); } // Add properties from root fields using propertyDefinitionMap ONLY if no properties were already processed if (!empty($propertyDefinitionMap) && !isset($data['properties'])) { @@ -1304,6 +1400,31 @@ private function addPropertiesToXml(\SimpleXMLElement $node, array $properties): } } + /** + * Add a child element with text content and optional xml:lang attribute + */ + private function addLangTextChild(\SimpleXMLElement $parent, string $tagName, $data): void + { + if ($data === null) { + return; + } + if (is_array($data)) { + $childNode = $parent->addChild($tagName); + if (isset($data['_value'])) { + $childNode[0] = (string)$data['_value']; + } + foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) { + if (isset($data[$langKey])) { + $childNode->addAttribute('xml:lang', $data[$langKey], 'http://www.w3.org/XML/1998/namespace'); + break; + } + } + } elseif (is_string($data) && $data !== '') { + $childNode = $parent->addChild($tagName); + $childNode[0] = $data; + } + } + /** * Extract model metadata from objects */ @@ -1327,35 +1448,47 @@ private function extractModelMetadata(array $objects): array */ private function addModelMetadataToXml(\SimpleXMLElement $xml, array $modelMetadata): void { - // Add name if present - if (isset($modelMetadata['name'])) { + // Prefer xml field data (preserves full array structure with xml:lang from import) + $xmlField = $modelMetadata['xml'] ?? []; + + // Resolve name: prefer xml field (array with _value/_xml__lang), fall back to flat field + $nameData = $xmlField['name'] ?? $modelMetadata['name'] ?? null; + if ($nameData !== null) { $nameNode = $xml->addChild('name'); - if (is_array($modelMetadata['name']) && isset($modelMetadata['name']['_value'])) { - $nameNode[0] = (string)$modelMetadata['name']['_value']; - if (isset($modelMetadata['name']['xml:lang'])) { - $nameNode->addAttribute('xml:lang', $modelMetadata['name']['xml:lang'], 'http://www.w3.org/XML/1998/namespace'); + if (is_array($nameData) && isset($nameData['_value'])) { + $nameNode[0] = (string)$nameData['_value']; + foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) { + if (isset($nameData[$langKey])) { + $nameNode->addAttribute('xml:lang', $nameData[$langKey], 'http://www.w3.org/XML/1998/namespace'); + break; + } } - } elseif (is_string($modelMetadata['name'])) { - $nameNode[0] = $modelMetadata['name']; + } elseif (is_string($nameData)) { + $nameNode[0] = $nameData; } } - // Add documentation if present - if (isset($modelMetadata['documentation'])) { + // Resolve documentation: prefer xml field, fall back to flat field + $docData = $xmlField['documentation'] ?? $modelMetadata['documentation'] ?? null; + if ($docData !== null) { $docNode = $xml->addChild('documentation'); - if (is_array($modelMetadata['documentation']) && isset($modelMetadata['documentation']['_value'])) { - $docNode[0] = (string)$modelMetadata['documentation']['_value']; - if (isset($modelMetadata['documentation']['xml:lang'])) { - $docNode->addAttribute('xml:lang', $modelMetadata['documentation']['xml:lang'], 'http://www.w3.org/XML/1998/namespace'); + if (is_array($docData) && isset($docData['_value'])) { + $docNode[0] = (string)$docData['_value']; + foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) { + if (isset($docData[$langKey])) { + $docNode->addAttribute('xml:lang', $docData[$langKey], 'http://www.w3.org/XML/1998/namespace'); + break; + } } - } elseif (is_string($modelMetadata['documentation'])) { - $docNode[0] = $modelMetadata['documentation']; + } elseif (is_string($docData)) { + $docNode[0] = $docData; } } - // Add properties if present - if (isset($modelMetadata['properties']) && is_array($modelMetadata['properties'])) { - $this->addPropertiesToXml($xml, $modelMetadata['properties']); + // Resolve properties: prefer xml field, fall back to flat field + $propsData = $xmlField['properties'] ?? $modelMetadata['properties'] ?? null; + if ($propsData !== null && is_array($propsData)) { + $this->addPropertiesToXml($xml, $propsData); } } diff --git a/lib/Service/ArchiMateImportService.php b/lib/Service/ArchiMateImportService.php index b8b256ea..4028e6c4 100644 --- a/lib/Service/ArchiMateImportService.php +++ b/lib/Service/ArchiMateImportService.php @@ -777,7 +777,20 @@ private function normalizeArchiMateData(array $data, string $modelIdentifier): a } } if ($sectionData !== null) { - $normalized[$section] = $this->extractSectionDataWithProperties($sectionData, $section, $modelIdentifier, $propertyDefinitionMap); + // Organizations are hierarchical folder trees, not flat objects with identifiers. + // Store the entire tree as one raw entry so round-trip export can reconstruct it. + if ($section === 'organizations') { + $syntheticId = 'org-' . preg_replace('/^id-/', '', $modelIdentifier); + $normalized[$section][$syntheticId] = [ + 'identifier' => $syntheticId, + 'section' => 'organization', + 'model_identifier' => $modelIdentifier, + 'name' => 'Organizations', + 'xml' => $sectionData // complete hierarchy preserved + ]; + } else { + $normalized[$section] = $this->extractSectionDataWithProperties($sectionData, $section, $modelIdentifier, $propertyDefinitionMap); + } } } $this->logger->info('Data normalization completed', [ @@ -976,6 +989,31 @@ private function createModelObject(array $metadata, string $modelIdentifier): ar $registerId = $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."); $schemaId = $this->cachedConfig['schemaIds']['model'] ?? throw new \RuntimeException("Schema ID for 'model' not found in cached configuration. Please ensure AMEF configuration is properly initialized."); + // Extract a plain string name (schema column expects string, not array) + $nameString = null; + if (isset($metadata['name'])) { + if (is_array($metadata['name']) && isset($metadata['name']['_value'])) { + $nameString = (string)$metadata['name']['_value']; + } elseif (is_string($metadata['name'])) { + $nameString = $metadata['name']; + } + } + + // Build xml field preserving full array structure for round-trip fidelity + $xmlData = []; + if (isset($metadata['name'])) { + $xmlData['name'] = $metadata['name']; + } + if (isset($metadata['documentation'])) { + $xmlData['documentation'] = $metadata['documentation']; + } + if (isset($metadata['properties'])) { + $xmlData['properties'] = $metadata['properties']; + } + if (isset($metadata['propertyDefinitionMap'])) { + $xmlData['propertyDefinitionMap'] = $metadata['propertyDefinitionMap']; + } + // Create object with @self structure and metadata at root level (no JSON serialization) $object = [ '@self' => [ @@ -988,11 +1026,17 @@ private function createModelObject(array $metadata, string $modelIdentifier): ar ], 'identifier' => $metadata['identifier'] ?? '', 'section' => 'model', - 'model_identifier' => $modelIdentifier + 'model_identifier' => $modelIdentifier, + 'xml' => $xmlData ]; - // Merge metadata directly at root level - return array_merge($object, $metadata); + // Merge metadata directly at root level, but override name with string version + $merged = array_merge($object, $metadata); + if ($nameString !== null) { + $merged['name'] = $nameString; + } + + return $merged; } /** @@ -1127,6 +1171,8 @@ private function saveObjectsToDatabase(array $objects): array 'unchanged' => [], 'invalid' => [], ]; + // Track counts per schema for accurate statistics (serialized objects lose the 'section' field) + $countsBySchema = []; foreach ($schemaGroups as $schemaId => $schemaObjects) { $schemaObjectCount = count($schemaObjects); @@ -1150,6 +1196,14 @@ private function saveObjectsToDatabase(array $objects): array $aggregatedStats['unchanged'] = array_merge($aggregatedStats['unchanged'], $saveResult['unchanged'] ?? []); $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []); + // Track per-schema counts for statistics (since serialized objects lose 'section') + $countsBySchema[$schemaId] = [ + 'saved' => count($saveResult['saved'] ?? []), + 'updated' => count($saveResult['updated'] ?? []), + 'unchanged' => count($saveResult['unchanged'] ?? []), + 'invalid' => count($saveResult['invalid'] ?? []), + ]; + $allResults = array_merge($allResults, $saveResult['saved'] ?? [], $saveResult['updated'] ?? [], $saveResult['unchanged'] ?? []); $this->logger->debug('Schema group saved for magic mapping', [ @@ -1167,7 +1221,8 @@ private function saveObjectsToDatabase(array $objects): array } } - // Store aggregated result for statistics + // Store aggregated result for statistics, including per-schema counts + $aggregatedStats['countsBySchema'] = $countsBySchema; $this->lastSaveResult = $aggregatedStats; $result = $allResults; @@ -1968,48 +2023,33 @@ private function buildStatisticsFromSaveResult(): array $saveResult = $this->lastSaveResult; - // Map singular section values (from import) to plural statistics keys - $sectionMap = [ - 'element' => 'elements', - 'relationship' => 'relationships', - 'organization' => 'organizations', - 'view' => 'views', - 'property_definition' => 'property_definitions', - 'model' => 'elements', // model objects fall under elements - ]; + // Use per-schema counts if available (reliable — objects are saved per-schema-group, + // so we know which schema each count belongs to without inspecting serialized objects). + if (!empty($saveResult['countsBySchema']) && $this->cachedConfig !== null) { + $sectionMap = [ + 'model' => 'elements', + 'element' => 'elements', + 'relationship' => 'relationships', + 'organization' => 'organizations', + 'view' => 'views', + 'property_definition' => 'property_definitions', + ]; - // Helper to get the statistics key for an object - $getSectionKey = function ($obj) use ($sectionMap): string { - if (is_object($obj) && method_exists($obj, 'jsonSerialize')) { - $obj = $obj->jsonSerialize(); + // Build reverse map: schema ID → plural section name + $schemaToSection = []; + foreach ($this->cachedConfig['schemaIds'] as $type => $schemaId) { + $schemaToSection[(int)$schemaId] = $sectionMap[$type] ?? 'elements'; } - $section = $obj['section'] ?? 'element'; - return $sectionMap[$section] ?? 'elements'; - }; - - // Categorize saved (created) objects by section - foreach ($saveResult['saved'] ?? [] as $obj) { - $key = $getSectionKey($obj); - $statistics[$key]['created']++; - } - - // Categorize updated objects by section - foreach ($saveResult['updated'] ?? [] as $obj) { - $key = $getSectionKey($obj); - $statistics[$key]['updated']++; - } - - // Categorize unchanged objects by section - foreach ($saveResult['unchanged'] ?? [] as $obj) { - $key = $getSectionKey($obj); - $statistics[$key]['unchanged']++; - } - // Count invalid objects as errors - foreach ($saveResult['invalid'] ?? [] as $invalidItem) { - $obj = $invalidItem['object'] ?? []; - $key = $getSectionKey($obj); - $statistics[$key]['errors'][] = $invalidItem['error'] ?? 'Unknown error'; + foreach ($saveResult['countsBySchema'] as $schemaId => $counts) { + $sectionKey = $schemaToSection[(int)$schemaId] ?? 'elements'; + $statistics[$sectionKey]['created'] += $counts['saved'] ?? 0; + $statistics[$sectionKey]['updated'] += $counts['updated'] ?? 0; + $statistics[$sectionKey]['unchanged'] += $counts['unchanged'] ?? 0; + if (($counts['invalid'] ?? 0) > 0) { + $statistics[$sectionKey]['errors'][] = "{$counts['invalid']} validation error(s)"; + } + } } // Calculate summary totals @@ -2051,36 +2091,31 @@ private function calculateOptimizedStatistics(array $savedObjects): array if ($this->lastSaveResult !== null) { $saveResult = $this->lastSaveResult; - // Map singular section values (from import) to plural statistics keys - $sectionMap = [ - 'element' => 'elements', - 'relationship' => 'relationships', - 'organization' => 'organizations', - 'view' => 'views', - 'property_definition' => 'property_definitions', - 'model' => 'elements', - ]; + // Use per-schema counts if available (reliable — doesn't depend on serialized object fields) + if (!empty($saveResult['countsBySchema']) && $this->cachedConfig !== null) { + $sectionMap = [ + 'model' => 'elements', + 'element' => 'elements', + 'relationship' => 'relationships', + 'organization' => 'organizations', + 'view' => 'views', + 'property_definition' => 'property_definitions', + ]; - $getSectionKey = function ($obj) use ($sectionMap): string { - if (is_object($obj) && method_exists($obj, 'jsonSerialize')) { - $obj = $obj->jsonSerialize(); + $schemaToSection = []; + foreach ($this->cachedConfig['schemaIds'] as $type => $schemaId) { + $schemaToSection[(int)$schemaId] = $sectionMap[$type] ?? 'elements'; } - $section = $obj['section'] ?? 'element'; - return $sectionMap[$section] ?? 'elements'; - }; - foreach ($saveResult['saved'] ?? [] as $obj) { - $statistics[$getSectionKey($obj)]['created']++; - } - foreach ($saveResult['updated'] ?? [] as $obj) { - $statistics[$getSectionKey($obj)]['updated']++; - } - foreach ($saveResult['unchanged'] ?? [] as $obj) { - $statistics[$getSectionKey($obj)]['unchanged']++; - } - foreach ($saveResult['invalid'] ?? [] as $invalidItem) { - $obj = $invalidItem['object'] ?? []; - $statistics[$getSectionKey($obj)]['errors'][] = $invalidItem['error'] ?? 'Unknown validation error'; + foreach ($saveResult['countsBySchema'] as $schemaId => $counts) { + $sectionKey = $schemaToSection[(int)$schemaId] ?? 'elements'; + $statistics[$sectionKey]['created'] += $counts['saved'] ?? 0; + $statistics[$sectionKey]['updated'] += $counts['updated'] ?? 0; + $statistics[$sectionKey]['unchanged'] += $counts['unchanged'] ?? 0; + if (($counts['invalid'] ?? 0) > 0) { + $statistics[$sectionKey]['errors'][] = "{$counts['invalid']} validation error(s)"; + } + } } // Calculate summary totals @@ -2636,22 +2671,26 @@ private function extractViewNodesRecursively($nodeData, array $elementsLookup = } } + // Add parent node BEFORE its children so the frontend rendering + // engine can look up parents via graph.getCell(parentId). + $viewNodes[] = $viewNode; + // Handle child nodes recursively (flatten hierarchy into single array while preserving parent-child relationships) if (isset($node['node'])) { $childNodes = $this->extractViewNodesRecursively($node['node'], $elementsLookup); - // IMPORTANT: Set parent reference for all child nodes to maintain hierarchy - // This allows the frontend to reconstruct the nested structure + // Set parent reference only for DIRECT children (those with parent === null). + // Grandchildren already have their parent set by the recursive call. foreach ($childNodes as &$childNode) { - $childNode['parent'] = $nodeId; // Parent is the current node's ID + if ($childNode['parent'] === null) { + $childNode['parent'] = $nodeId; + } } unset($childNode); // Add child nodes to the main flattened array (maintaining parent references) $viewNodes = array_merge($viewNodes, $childNodes); } - - $viewNodes[] = $viewNode; } return $viewNodes; @@ -2885,13 +2924,18 @@ private function applyNodeStyle(array &$viewNode, array $style): void $viewNode['color'] = "rgb($r, $g, $b)"; } - // Extract lineColor + // Extract lineColor (including alpha for border visibility) if (isset($style['lineColor']['_attributes'])) { $lineColor = $style['lineColor']['_attributes']; $r = isset($lineColor['r']) ? (int)$lineColor['r'] : 0; $g = isset($lineColor['g']) ? (int)$lineColor['g'] : 0; $b = isset($lineColor['b']) ? (int)$lineColor['b'] : 0; - $viewNode['borderColor'] = "rgb($r, $g, $b)"; + $a = isset($lineColor['a']) ? (int)$lineColor['a'] : 100; + if ($a < 100) { + $viewNode['borderColor'] = "rgba($r, $g, $b, " . round($a / 100, 2) . ")"; + } else { + $viewNode['borderColor'] = "rgb($r, $g, $b)"; + } } // Extract font information @@ -3833,23 +3877,11 @@ private function transformViewsOptimized( if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) { $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap); - // Update ID and slug if objectId is available - if (isset($object['objectId'])) { - $object['@self']['id'] = $object['objectId']; - $object['@self']['slug'] = $identifier; - } else { - if ($identifier && str_starts_with($identifier, 'id-')) { - $object['@self']['slug'] = substr($identifier, 3); - } else { - $object['@self']['slug'] = $identifier; - } - } + // Keep @self.id as the full ArchiMate identifier (set above) + // so stored IDs match GEMMA Online URLs (id-e0f57689-...). + $object['@self']['slug'] = $identifier; } else { - if ($identifier && str_starts_with($identifier, 'id-')) { - $object['@self']['slug'] = substr($identifier, 3); - } else { - $object['@self']['slug'] = $identifier; - } + $object['@self']['slug'] = $identifier; } // Copy viewNodes and viewRelationships from XML to root level for easy access @@ -4026,19 +4058,52 @@ private function createModelObjectDirect(array $metadata, string $modelIdentifie { $organisation = $this->getCurrentOrganisation(); - return [ + // Extract a plain string name (schema column expects string, not array) + $nameString = null; + if (isset($metadata['name'])) { + if (is_array($metadata['name']) && isset($metadata['name']['_value'])) { + $nameString = (string)$metadata['name']['_value']; + } elseif (is_string($metadata['name'])) { + $nameString = $metadata['name']; + } + } + + // Build xml field preserving full array structure for round-trip fidelity + $xmlData = []; + if (isset($metadata['name'])) { + $xmlData['name'] = $metadata['name']; + } + if (isset($metadata['documentation'])) { + $xmlData['documentation'] = $metadata['documentation']; + } + if (isset($metadata['properties'])) { + $xmlData['properties'] = $metadata['properties']; + } + if (isset($metadata['propertyDefinitionMap'])) { + $xmlData['propertyDefinitionMap'] = $metadata['propertyDefinitionMap']; + } + + $object = [ '@self' => [ 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."), 'schema' => $this->cachedConfig['schemaIds']['model'] ?? throw new \RuntimeException("Schema ID for 'model' not found in cached configuration. Please ensure AMEF configuration is properly initialized."), 'id' => $modelIdentifier, 'owner' => $this->cachedConfig['userId'], - 'organisation' => $organisation, // Use the method instead of cached value + 'organisation' => $organisation, 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $modelIdentifier, 'section' => 'model', - 'model_identifier' => $modelIdentifier + 'model_identifier' => $modelIdentifier, + 'xml' => $xmlData ] + $metadata; + + // Override name with string version so schema column stores it properly + if ($nameString !== null) { + $object['name'] = $nameString; + } + + return $object; } /** @@ -4422,6 +4487,30 @@ private function bulkProcessNonViewSections( ]; foreach ($sections as $sectionName => $schemaType) { + // Organizations are hierarchical folder trees — store as one tree object + if ($sectionName === 'organizations') { + $orgData = $this->findSectionData($xmlData, 'organizations'); + if (!empty($orgData)) { + $syntheticId = 'org-' . preg_replace('/^id-/', '', $modelIdentifier); + $objects[] = [ + '@self' => [ + 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration."), + 'schema' => $this->cachedConfig['schemaIds']['organization'] ?? throw new \RuntimeException("Schema ID for 'organization' not found."), + 'id' => $syntheticId, + 'owner' => $this->cachedConfig['userId'], + 'organisation' => $this->getCurrentOrganisation(), + 'published' => date('Y-m-d\TH:i:s\Z') + ], + 'identifier' => $syntheticId, + 'section' => 'organization', + 'model_identifier' => $modelIdentifier, + 'name' => 'Organizations', + 'xml' => $orgData + ]; + } + continue; + } + if (empty($allLookups[$sectionName])) continue; $this->logger->debug("SPEED: Bulk processing {$sectionName}", [ @@ -4644,18 +4733,11 @@ private function bulkTransformViews( if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) { $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap); - if (isset($object['objectId'])) { - $object['@self']['id'] = $object['objectId']; - $object['@self']['slug'] = $identifier; - } else { - $object['@self']['slug'] = str_starts_with($identifier, 'id-') - ? substr($identifier, 3) - : $identifier; - } + // Keep @self.id as the full ArchiMate identifier (set above) + // so stored IDs match GEMMA Online URLs (id-e0f57689-...). + $object['@self']['slug'] = $identifier; } else { - $object['@self']['slug'] = str_starts_with($identifier, 'id-') - ? substr($identifier, 3) - : $identifier; + $object['@self']['slug'] = $identifier; } // SPEED OPTIMIZATION: Direct copy without checks (we know it exists) @@ -4871,18 +4953,39 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $object = $object->jsonSerialize(); } - $sectionType = $object['section'] ?? 'elements'; // Default to elements if section not found + $sectionType = $object['section'] ?? null; - // Map section types to statistics keys + // Map section types (singular or plural) to statistics keys $sectionKey = match($sectionType) { - 'elements' => 'elements', - 'relationships' => 'relationships', - 'organizations' => 'organizations', - 'views' => 'views', - 'property_definitions' => 'property_definitions', - default => 'elements' // Default fallback + 'elements', 'element', 'model' => 'elements', + 'relationships', 'relationship' => 'relationships', + 'organizations', 'organization' => 'organizations', + 'views', 'view' => 'views', + 'property_definitions', 'property_definition' => 'property_definitions', + default => null }; + // Fallback: use @self.schema to determine section + if ($sectionKey === null && $this->cachedConfig !== null && isset($this->cachedConfig['schemaIds'])) { + $objSchemaId = $object['@self']['schema'] ?? null; + if ($objSchemaId !== null) { + $singularToPlural = [ + 'element' => 'elements', 'relationship' => 'relationships', + 'organization' => 'organizations', 'view' => 'views', + 'property_definition' => 'property_definitions', 'model' => 'elements', + ]; + foreach ($this->cachedConfig['schemaIds'] as $type => $schemaId) { + if ((int)$schemaId === (int)$objSchemaId) { + $sectionKey = $singularToPlural[$type] ?? 'elements'; + break; + } + } + } + $sectionKey = $sectionKey ?? 'elements'; + } elseif ($sectionKey === null) { + $sectionKey = 'elements'; + } + if (!isset($statistics[$sectionKey])) { continue; // Skip unknown section types } diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index cc70b1bf..9e68fb52 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -198,12 +198,11 @@ private function getViewsFromRegister(): array '@self' => [ 'register' => $registerId, 'schema' => $viewSchemaId - ], - 'section' => 'view' + ] ]; $views = $objectService->searchObjects($query); - + $this->logger->debug('Retrieved views from register', [ 'register_id' => $registerId, 'view_schema_id' => $viewSchemaId, @@ -238,7 +237,7 @@ private function getViewFromRegister(string $viewId): ?array try { // Get specific view object by ID $view = $objectService->getObject($registerId, $viewSchemaId, $viewId); - + $this->logger->debug('Retrieved specific view from register', [ 'view_id' => $viewId, 'register_id' => $registerId, diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index e18dd386..750b30cc 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -439,7 +439,7 @@ { "title": "Common Ground", "summary": "Een nieuwe manier van werken met informatiesystemen binnen de overheid", - "description": "Common Ground is een initiatief dat streeft naar een moderne, flexibele en effici\u00ebnte informatievoorziening voor gemeenten. Het principe is gebaseerd op het scheiden van data en applicaties, waardoor informatie eenvoudiger kan worden gedeeld en hergebruikt.", + "description": "Common Ground is een initiatief dat streeft naar een moderne, flexibele en efficiënte informatievoorziening voor gemeenten. Het principe is gebaseerd op het scheiden van data en applicaties, waardoor informatie eenvoudiger kan worden gedeeld en hergebruikt.", "organization": "VNG Realisatie", "themes": [ "Architectuur", @@ -511,7 +511,9 @@ "summary": "Aanvullende afspraak bij het convenant met nadere detaillering voor één standaard", "description": "Voor elke op het convenant aanvullende afspraak die nadere detaillering behoeft, worden losse addenda (aanvullingen) aan het convenant gehangen. In een addendum wordt voor 1 standaard een realisatietermijn afgesproken. Leveranciers streven ernaar de betreffende standaard tijdig en juist in de softwareproducten in te bouwen, te implementeren en te laten gebruiken door gemeenten.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Addendum", - "keywords": ["Addenda"], + "keywords": [ + "Addenda" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -524,7 +526,10 @@ "summary": "Algemene samenwerkingsafspraken met leveranciers, gebruikersverenigingen en samenwerkingsverbanden", "description": "Hierin zijn algemene samenwerkingsafspraken vastgelegd met leveranciers, gebruikersverenigingen, en samenwerkingsverbanden die voor gemeenten software inkopen. De samenwerking is gericht op het gezamenlijk (door)ontwikkelen van procesmatige en technische standaarden o.a. door vakmatig kennis te delen.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Convenant%20VNG", - "keywords": ["VNG-convenant", "convenant"], + "keywords": [ + "VNG-convenant", + "convenant" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -537,7 +542,9 @@ "summary": "Standaard waarbij functionaliteit, services en rolverdeling scherp gedefinieerd zijn", "description": "Hierbij zijn de functionaliteit, de (web)services en rolverdeling tussen systemen gedetailleerd en scherp gedefinieerd. Juiste toepassing is preventief goed testbaar en daardoor is de voorspelbaarheid op goed functioneren hoog.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Eindproduct-standaard", - "keywords": ["Eindproduct-standaarden"], + "keywords": [ + "Eindproduct-standaarden" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -550,7 +557,10 @@ "summary": "Afspraken over meervoudig gebruik en uitwisseling van gegevens", "description": "Om ervoor te zorgen dat de uitgewisselde gegevens juist zijn en op de juiste manier worden gebruikt, met de juiste betekenis, dienen er zeer goede afspraken gemaakt te worden. De afspraken over het meervoudig gebruik van gegevens en de (gemeentelijke) gegevensuitwisseling zijn vastgelegd in gegevensstandaarden. Ook hiervoor geldt dat de leverancier dient aan te geven op welke wijze hieraan wordt voldaan.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Gegevensstandaard", - "keywords": ["Gegevens-standaarden", "Gegevensstandaarden"], + "keywords": [ + "Gegevens-standaarden", + "Gegevensstandaarden" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -561,9 +571,11 @@ { "title": "Grondstof-standaard", "summary": "Standaard die specialistische kennis en uitgebreide specificaties vereist", - "description": "Hierbij moeten n\u00f3g meer dan bij half-fabrikaten specificaties en activiteiten worden uitgevoerd om interoperabiliteit te kunnen realiseren. Dit vergt specialistische kennis.", + "description": "Hierbij moeten nóg meer dan bij half-fabrikaten specificaties en activiteiten worden uitgevoerd om interoperabiliteit te kunnen realiseren. Dit vergt specialistische kennis.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Grondstof-standaard", - "keywords": ["Grondstof-standaarden"], + "keywords": [ + "Grondstof-standaarden" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -576,7 +588,9 @@ "summary": "Standaard waarbij de opdrachtgever aanvullende specificaties moet toevoegen", "description": "Hierbij dient de opdrachtgever aanvullende specificaties toe te voegen.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Halffabrikaat-standaard", - "keywords": ["Halffabrikaat-standaarden"], + "keywords": [ + "Halffabrikaat-standaarden" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -589,7 +603,9 @@ "summary": "Aanbieder van standaard-softwarepakketten voor gemeentelijke taken", "description": "Aanbieders van standaard-software(pakketten) voor gemeentelijke taken. Alleen leveranciers die het convenant met VNG ondertekend hebben, mogen hun gegevens in de Softwarecatalogus zetten.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Leverancier", - "keywords": ["Leveranciers"], + "keywords": [ + "Leveranciers" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -600,9 +616,11 @@ { "title": "Referentiecomponent", "summary": "Verbinding tussen softwareaanbod van leveranciers en softwaregebruik van gemeenten", - "description": "De verbinding tussen softwareaanbod leveranciers en softwaregebruik gemeenten. Een softwarepakket kan functionaliteit bieden voor \u00e9\u00e9n of meerdere referentiecomponenten, daarmee krijgt u een globaal beeld van de functionaliteit.", + "description": "De verbinding tussen softwareaanbod leveranciers en softwaregebruik gemeenten. Een softwarepakket kan functionaliteit bieden voor één of meerdere referentiecomponenten, daarmee krijgt u een globaal beeld van de functionaliteit.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Referentiecomponent", - "keywords": ["Referentiecomponenten"], + "keywords": [ + "Referentiecomponenten" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -613,9 +631,11 @@ { "title": "SaaS", "summary": "Software as a Service: applicatie aangeboden vanuit de cloud door een leverancier", - "description": "Bij Software as a Service maakt een gemeente of samenwerking gebruik van een applicatie die door een leverancier vanuit de cloud wordt aangeboden. De gemeente of samenwerking heeft g\u00e9\u00e9n toegang tot de onderliggende infrastructuur en software, oftewel het technisch beheer wordt geheel door de leverancier verzorgd.", + "description": "Bij Software as a Service maakt een gemeente of samenwerking gebruik van een applicatie die door een leverancier vanuit de cloud wordt aangeboden. De gemeente of samenwerking heeft géén toegang tot de onderliggende infrastructuur en software, oftewel het technisch beheer wordt geheel door de leverancier verzorgd.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#SaaS", - "keywords": ["Software as a Service"], + "keywords": [ + "Software as a Service" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -628,7 +648,9 @@ "summary": "Norm of set van eisen waaraan gemeentelijke software moet voldoen", "description": "Norm of set van eisen/voorwaarden/afspraken waaraan (gemeentelijke) software moet voldoen. (Open) Standaarden zijn opgenomen die voor gemeenten van belang zijn voor interne en externe applicatiekoppelingen, gegevensmanagement, bedrijfsvoering en digitale dienstverlening. Standaarden kunnen als producten worden getypeerd in eindproducten, halffabrikaten, grondstoffen en gegevensstandaarden. Deze typering is een indicatie wat de benodigde inspanning is om op basis van de standaard tot een werkende koppeling te komen. VNG adviseert gemeenten bij voorkeur functioneel passende eindproduct-standaarden te gebruiken en pas daarna (in aflopende prioriteit) halffabrikaat- en grondstof-standaarden, en als laatste maatwerk toe te passen.", "externalLink": "https://www.softwarecatalogus.nl/lexicon#Standaard", - "keywords": ["Standaarden"], + "keywords": [ + "Standaarden" + ], "@self": { "configuration": "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/ConductionNL/opencatalogi/blob/master/apps-extra/opencatalogi/lib/Settings/publication_register_magic.json", "register": "publication", @@ -1755,7 +1777,10 @@ "enum": [], "example": "Bijvoorbeeld: [\"Aanbod-beheerder\", \"Functioneel-beheerder\"]", "authorization": { - "update": ["gebruik-beheerder", "admin"] + "update": [ + "gebruik-beheerder", + "admin" + ] } }, "e-mailadres": { @@ -2204,7 +2229,8 @@ "Leverancier", "Samenwerking", "Community" - ] + ], + "hideOnCollection": true }, "organisatieType": { "description": "Type van de organisatie (Gemeente, Leverancier, Samenwerking)", @@ -2262,7 +2288,6 @@ "title": "Samenwerkingstype", "type": "string", "visible": false, - "hideOnCollection": true, "facetable": true, "order": 14, "minLength": null, @@ -2399,7 +2424,7 @@ "AVG compliance document", "ESPD (Europees aanbestedingsdocument)", "Integriteitsverklaring", - "Financi\u00eble capaciteitsverklaring", + "Financiële capaciteitsverklaring", "Technische capaciteitsverklaring", "Kwaliteitscertificaten", "Milieucertificaten", @@ -2547,7 +2572,7 @@ "example": "Bijvoorbeeld: 2025-03-01" }, "startDatumUitTeFaseren": { - "description": "De start datum voor het \"Be\u00ebindigd\" status", + "description": "De start datum voor het \"Beëindigd\" status", "type": "string", "format": "date", "visible": true, @@ -3122,11 +3147,13 @@ "uri": null, "slug": "koppeling", "title": "Koppeling", - "description": "Schema voor koppelingen tussen applicaties en systemen. Er moet \u00f3f ApplicatieB \u00f3f buitengemeentelijkVoorziening gevuld zijn.", + "description": "Schema voor koppelingen tussen applicaties en systemen. Er moet óf ApplicatieB óf buitengemeentelijkVoorziening gevuld zijn.", "version": "0.1.0", "summary": "", "icon": "Link", - "required": [], + "required": [ + "naam" + ], "properties": { "naam": { "description": "Naam van de koppeling, default waarde [AppA] [<-richting->] [AppB]", @@ -3135,6 +3162,8 @@ "required": true, "facetable": false, "title": "Naam", + "default": "{{ moduleA }} {{ gegevensuitwisselingRichting | map: AnaarB=→, BnaarA=←, bi-directioneel=↔ }} {{ moduleB }}", + "defaultBehavior": "falsy", "table": { "default": true }, @@ -3256,7 +3285,7 @@ "type": "object", "order": 9, "facetable": false, - "required": true, + "required": false, "table": { "default": true }, @@ -3270,7 +3299,7 @@ "moduleB": { "description": "Kies de applicatie of buitengemeentelijke voorziening waarmee gekoppeld wordt.", "type": "object", - "required": true, + "required": false, "items": { "oneOf": [ { @@ -6396,8 +6425,7 @@ "summary": "", "icon": "Package", "required": [ - "naam", - "beschrijvingKort" + "naam" ], "properties": { "naam": { @@ -6419,7 +6447,7 @@ "title": "Korte omschrijving", "order": 2, "facetable": false, - "required": true, + "required": false, "maxLength": 255, "table": { "default": true @@ -6440,7 +6468,7 @@ "description": "Een URL naar uw applicatie. Default website van de organisatie", "type": "string", "format": "url", - "required": true, + "required": false, "visible": true, "order": 4, "maxLength": 500,