Skip to content

Commit

Permalink
FEATURE: support for referenced diagrams
Browse files Browse the repository at this point in the history
  • Loading branch information
skurfuerst committed Jan 23, 2024
1 parent 6e1b14b commit 4e71ae9
Show file tree
Hide file tree
Showing 15 changed files with 409 additions and 2 deletions.
18 changes: 17 additions & 1 deletion Classes/Controller/DiagramEditorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
use Neos\Flow\ResourceManagement\ResourceManager;
use Neos\Media\Domain\Model\Asset;
use Neos\Media\Domain\Model\Image;
use Neos\Media\Domain\Model\ImageInterface;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Sandstorm\MxGraph\DiagramIdentifierSearchService;
use Sandstorm\MxGraph\Domain\Model\Diagram;

class DiagramEditorController extends ActionController
{
Expand All @@ -18,6 +19,12 @@ class DiagramEditorController extends ActionController
*/
protected $resourceManager;

/**
* @Flow\Inject
* @var DiagramIdentifierSearchService
*/
protected $diagramIdentifierSearchService;

/**
* @Flow\InjectConfiguration(path="drawioEmbedUrl")
* @var string
Expand Down Expand Up @@ -88,6 +95,15 @@ public function saveAction(NodeInterface $node, $xml, $svg)
// NEW since version 3.0.0
$node->setProperty('diagramSvgText', $svg);

$diagramIdentifier = $node->getProperty('diagramIdentifier');
if (!empty($diagramIdentifier)) {
// also update related diagrams
foreach ($this->diagramIdentifierSearchService->findRelatedDiagramsWithIdentifierExcludingOwn($diagramIdentifier, $node) as $relatedDiagramNode) {
$relatedDiagramNode->setProperty('diagramSource', $xml);
$relatedDiagramNode->setProperty('diagramSvgText', $svg);
}
}

// BEGIN DEPRECATION since version 3.0.0
$persistentResource = $this->resourceManager->importResourceFromContent($svg, 'diagram.svg');

Expand Down
15 changes: 15 additions & 0 deletions Classes/DiagramCreationHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Sandstorm\MxGraph;

use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Neos\Ui\NodeCreationHandler\NodeCreationHandlerInterface;
use Sandstorm\MxGraph\Domain\Model\Diagram;

class DiagramCreationHandler implements NodeCreationHandlerInterface
{
public function handle(NodeInterface $node, array $data)
{
$node->setProperty('diagramIdentifier', 'Diagram ' . date('Y-m-d H:i'));
}
}
63 changes: 63 additions & 0 deletions Classes/DiagramIdentifierDataSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Sandstorm\MxGraph;

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\I18n\Translator;
use Neos\Neos\Service\DataSource\AbstractDataSource;
use Sandstorm\LazyDataSource\LazyDataSourceTrait;

class DiagramIdentifierDataSource extends AbstractDataSource
{

use LazyDataSourceTrait;

static protected $identifier = 'drawio-diagram-identifier';

/**
* @Flow\Inject
* @var Translator
*/
protected $translator;

/**
* @Flow\Inject
* @var DiagramIdentifierSearchService
*/
protected $diagramIdentifierSearchService;

protected function getDataForIdentifiers(array $identifiers, NodeInterface $node = null, array $arguments = [])
{
// all identifiers will be returned as is (with a label containing usage count)
$options = [];
foreach ($identifiers as $id) {
$options[$id] = ['label' => $this->getLabelFor($id, $node)];
}
return $options;
}

protected function searchData(string $searchTerm, NodeInterface $node = null, array $arguments = [])
{
$options = [];
if ($node !== null) {
$diagramIdentifiers = $this->diagramIdentifierSearchService->findInIdentifier($searchTerm, $node);
foreach ($diagramIdentifiers as $diagramIdentifier) {
$options[$diagramIdentifier] = ['label' => $this->getLabelFor($diagramIdentifier, $node)];
}
}

$options[$searchTerm] = ['label' => $searchTerm, 'icon' => 'plus'];
return $options;
}

protected function getLabelFor(string $diagramIdentifier, $node): string
{
$relatedCount = count($this->diagramIdentifierSearchService->findRelatedDiagramsWithIdentifierExcludingOwn($diagramIdentifier, $node));
$label = $diagramIdentifier;
if ($relatedCount > 0) {
$label .= $this->translator->translateById('diagramIdentifierUsageLabel', [$relatedCount+1], null, null, 'Main', 'Sandstorm.MxGraph');
}
return $label;
}
}
94 changes: 94 additions & 0 deletions Classes/DiagramIdentifierSearchService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

namespace Sandstorm\MxGraph;


use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Neos\Domain\Service\NodeSearchServiceInterface;

/**
* @Flow\Scope("singleton")
*/
class DiagramIdentifierSearchService
{

/**
* @Flow\Inject
* @var NodeSearchServiceInterface
*/
protected $nodeSearchService;

/**
* @param string $query
* @param NodeInterface $node
* @return string[]
*/
public function findInIdentifier(string $searchTerm, NodeInterface $node): array
{
$results = [];
$possibleResults = $this->nodeSearchService->findByProperties($searchTerm, ['Sandstorm.MxGraph:Diagram'], $node->getContext());
foreach ($possibleResults as $possibleResult) {
assert($possibleResult instanceof NodeInterface);

// we include the diagram identifier if it contains the $searchTerm (case-insensitively)
$possibleDiagramIdentifier = $possibleResult->getProperty('diagramIdentifier');
if (str_contains(strtolower($possibleDiagramIdentifier), strtolower($searchTerm))) {
if (!isset($results[$possibleDiagramIdentifier])) {
$results[$possibleDiagramIdentifier] = $possibleDiagramIdentifier;
}
}
}

return array_values($results);
}

/**
* @param string $diagramIdentifier
* @return NodeInterface[]
*/
public function findRelatedDiagramsWithIdentifierExcludingOwn(string $diagramIdentifier, NodeInterface $contextNode): array
{
$results = [];
$possibleResults = $this->nodeSearchService->findByProperties($diagramIdentifier, ['Sandstorm.MxGraph:Diagram'], $contextNode->getContext());
foreach ($possibleResults as $node) {
assert($node instanceof NodeInterface);
if ($contextNode->getContextPath() === $node->getContextPath()) {
// we skip ourselves
continue;
}

// we still need to check for exact DiagramIdentifier, because nodeSearchService finds across **ALL** properties
// and with wildcards.
if ($node->getProperty('diagramIdentifier') === $diagramIdentifier) {
$results[] = $node;
}
}

return $results;
}

/**
* @param string $diagramIdentifier
* @param NodeInterface $contextNode
* @return NodeInterface|null
*/
public function findMostRecentDiagramWithIdentifierExcludingOwn(string $diagramIdentifier, NodeInterface $contextNode): ?NodeInterface
{
$relatedDiagramNodes = $this->findRelatedDiagramsWithIdentifierExcludingOwn($diagramIdentifier, $contextNode);

uasort($relatedDiagramNodes, function(NodeInterface $a, NodeInterface $b) {
if (method_exists($a, 'getLastModificationDateTime') && method_exists($b, 'getLastModificationDateTime')) {
return $b->getLastModificationDateTime() <=> $a->getLastModificationDateTime();
}

return 0;
});

if (count($relatedDiagramNodes) > 0) {
// the 1st element is the one with the latest modification time.
return reset($relatedDiagramNodes);
}
return null;
}
}
50 changes: 50 additions & 0 deletions Classes/DiagramNodeHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Sandstorm\MxGraph;

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\NodeInterface;

class DiagramNodeHandler
{
/**
* @Flow\Inject
* @var DiagramIdentifierSearchService
*/
protected $diagramIdentifierSearchService;

/**
* Signals that the property of a node was changed.
*
* @Flow\Signal
* @param NodeInterface $node
* @param string $propertyName name of the property that has been changed/added
* @param mixed $oldValue the property value before it was changed or NULL if the property is new
* @param mixed $newValue the new property value
* @return void
*/
public function nodePropertyChanged(NodeInterface $node, $propertyName, $oldValue, $newValue)
{
if (!$node->getNodeType()->isOfType('Sandstorm.MxGraph:Diagram')) {
return;
}

if ($propertyName !== 'diagramIdentifier') {
return;
}

if ($oldValue === $newValue) {
return;
}

if (empty($newValue)) {
return;
}

$sourceDiagramNode = $this->diagramIdentifierSearchService->findMostRecentDiagramWithIdentifierExcludingOwn($newValue, $node);
if ($sourceDiagramNode) {
$node->setProperty('diagramSource', $sourceDiagramNode->getProperty('diagramSource'));
$node->setProperty('diagramSvgText', $sourceDiagramNode->getProperty('diagramSvgText'));
}
}
}
20 changes: 20 additions & 0 deletions Classes/Package.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
namespace Sandstorm\MxGraph;

use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Package\Package as BasePackage;
use Neos\ContentRepository\Domain\Model\Node;

class Package extends BasePackage
{
/**
* @param Bootstrap $bootstrap The current bootstrap
* @return void
*/
public function boot(Bootstrap $bootstrap)
{
$dispatcher = $bootstrap->getSignalSlotDispatcher();

$dispatcher->connect(Node::class, 'nodePropertyChanged', DiagramNodeHandler::class, 'nodePropertyChanged');
}
}
19 changes: 19 additions & 0 deletions Configuration/NodeTypes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
superTypes:
'Neos.Neos:Content': true
'Neos.NodeTypes.BaseMixins:ContentImageMixin': true
options:
nodeCreationHandlers:
- nodeCreationHandler: Sandstorm\MxGraph\DiagramCreationHandler
ui:
label: Diagram
icon: icon-picture
Expand All @@ -21,3 +24,19 @@
inspector:
editor: Sandstorm.MxGraph/Editors/DiagramEditor
group: image
# BEGINNING with version 3.1
diagramIdentifier:
type: string
ui:
label: i18n
# if the diagram Identifier changed, the DiagramNodeHandler might have updated diagramSvgText and diagramSource; thus we need to refresh.
reloadIfChanged: true
inspector:
group: image
editor: 'Sandstorm.LazyDataSource/Inspector/Editors/DataSourceSelectEditor'
editorOptions:
placeholder: Create new or select ...
dataSourceIdentifier: drawio-diagram-identifier
dataSourceDisableCaching: true
help:
message: i18n
4 changes: 4 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Neos:
patternOptions:
controllerObjectNamePattern: 'Sandstorm\MxGraph\Controller\.*'
Neos:
userInterface:
translation:
autoInclude:
'Sandstorm.MxGraph': ['NodeTypes/*']
fusion:
autoInclude:
Sandstorm.MxGraph: true
Expand Down
45 changes: 45 additions & 0 deletions Documentation/2024_01_22_ReusableDiagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Draw.io - Reusable Diagrams

## Goal

Diagrams should be **referenceable** by different pages, instead of being **copied**.

## Idea 1: attach to **ImageInterface**

Benefits:
- **Asset Usage** can be used to track duplicates.

Notes:
- There is a workflow to replace assets, by "Replace Asset Resource", BUT this will DIRECTLY
replace the LIVE WORKSPACE ASSET; so it will break workspace encapsulation.

## Idea 1a: via separate domain model

This was the original idea; but using a subclass of Image or Asset would be even more convenient I think.

## Idea 2: Custom referencing, manual updates

Assume we have a property "identifier" on the Diagram node, with the following rules:

- when updating the diagram, we update the diagram on **all nodes with the same identifier**
- when updating the identifier, you get an autocomplete for existing identifiers in the system.
- we can use https://github.com/sandstorm/LazyDataSource here for this (for server side search)
- ... and a new entry "create new" (or s.th. like it)
- If the identifier changes TO ANOTHER EXISTING identifier, we copy the other diagram over to this node.
- If there are multiple with the same identifier, we use the **newest** one.
- `nodePropertyChanged` event

- You get an indicator how often this identifier is used. With NodeType View??


## ToDo

- [x] Autocomplete for identifier
- [x] LazyDataSource
- [ ] some issues in re-rendering in LazyDataSource
- [x] if Identifier changed and you press APPLY, the diagram is updated.
- [WONT] if Identifier changed and you open the diagram editor, the other diagram is shown
- !!! we don't do this, as it is too easy to accidentally overwrite stuff.
- [x] when saving a diagram with non-empty identifier, update all diagram targets
- [x] a new identifier is generated on creation
- [ ] display number of references
10 changes: 10 additions & 0 deletions Resources/Private/Translations/de/Main.xlf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file original="" product-name="Sandstorm.MxGraph" source-language="en" datatype="plaintext">
<body>
<trans-unit id="diagramIdentifierUsageLabel" xml:space="preserve">
<source> ({0} mal verwendet)</source>
</trans-unit>
</body>
</file>
</xliff>
Loading

0 comments on commit 4e71ae9

Please sign in to comment.